Release catalyst

This commit is contained in:
Jonas Plum
2021-12-13 00:39:15 +01:00
commit 15cf0ebd49
339 changed files with 111677 additions and 0 deletions

342
generator/generator.go Normal file
View File

@@ -0,0 +1,342 @@
package main
import (
"bytes"
"embed"
"encoding/json"
"flag"
"fmt"
"go/format"
"log"
"net/url"
"os"
"path"
"strings"
"text/template"
"github.com/go-openapi/analysis"
"github.com/go-swagger/go-swagger/generator"
"github.com/iancoleman/strcase"
"github.com/tidwall/sjson"
"gopkg.in/yaml.v3"
)
//go:embed templates/simplemodel.gotmpl
var model embed.FS
func gotype(name string, s Schema, required []string) string {
_, x := sgotype(name, s, required, false)
return x
}
func sgotype(name string, s Schema, required []string, nopointer bool) (bool, string) {
req := ""
if !nopointer && !contains(required, name) {
req = "*"
}
if s.Ref != "" {
return false, req + path.Base(s.Ref)
}
primitive := false
t := ""
switch s.Type {
case "string":
if s.Format == "date-time" {
t = req + "time.Time"
} else {
t = req + "string"
primitive = true
}
case "boolean":
t = req + "bool"
primitive = true
case "object":
if s.AdditionalProperties != nil {
subPrimitive, subType := sgotype(name, *s.AdditionalProperties, required, true)
if subPrimitive {
t = "map[string]" + subType
} else {
t = "map[string]*" + subType
}
} else {
t = "interface{}"
}
case "number", "integer":
if s.Format != "" {
t = req + s.Format
} else {
t = req + "int"
}
primitive = true
case "array":
subPrimitive, subType := sgotype(name, *s.Items, required, true)
if subPrimitive {
t = "[]" + subType
} else {
t = "[]*" + subType
}
case "":
t = "interface{}"
default:
panic(fmt.Sprintf("%#v", s))
}
return primitive, t
}
func omitempty(name string, required []string) bool {
return !contains(required, name)
}
func contains(required []string, name string) bool {
for _, r := range required {
if r == name {
return true
}
}
return false
}
func tojson(name string, i Definition) string {
b, _ := json.Marshal(i)
b, _ = sjson.SetBytes(b, "$id", "#/definitions/"+name)
return string(b)
}
func camel(s string) string {
if s == "id" {
return "ID"
}
return strcase.ToCamel(s)
}
func main() {
flag.Parse()
p := flag.Arg(0)
log.SetFlags(log.LstdFlags | log.Lshortfile)
f, err := os.Open("generated/community.yml")
if err != nil {
log.Fatalln(err)
}
defer f.Close()
s := Swagger{}
dec := yaml.NewDecoder(f)
err = dec.Decode(&s)
if err != nil {
log.Fatalln(err)
}
t := template.New("simplemodel.gotmpl")
t.Funcs(map[string]interface{}{
"camel": camel,
"gotype": gotype,
"omitempty": omitempty,
"tojson": tojson,
})
templ := template.Must(t.ParseFS(model, "templates/simplemodel.gotmpl"))
err = os.MkdirAll("generated/models", os.ModePerm)
if err != nil {
log.Fatalln(err)
}
buf := bytes.NewBufferString("")
props := map[string][]string{}
for defName, definition := range s.Definitions {
for propName := range definition.Properties {
props[defName] = append(props[defName], propName)
}
}
// for _, definition := range s.Definitions {
// if definition.Embed != "" {
// if parentProps, ok := props[definition.Embed]; ok {
// for _, parentProp := range parentProps {
// delete(definition.Properties, parentProp)
// }
// }
// }
// }
err = templ.Execute(buf, &s)
if err != nil {
log.Fatalln(err)
}
fmtCode, err := format.Source(buf.Bytes())
if err != nil {
log.Println(err)
fmtCode = buf.Bytes()
}
err = os.WriteFile("generated/models/models.go", fmtCode, os.ModePerm)
if err != nil {
log.Fatalln(err)
}
generator.FuncMapFunc = func(opts *generator.LanguageOpts) template.FuncMap {
df := generator.DefaultFuncMap(opts)
df["path"] = func(basePath, lpath string, parameters generator.GenParameters) string {
u := url.URL{Path: path.Join(basePath, lpath)}
q := u.Query()
for _, p := range parameters {
if p.Location == "path" {
if example, ok := p.Extensions["x-example"]; ok {
u.Path = strings.ReplaceAll(u.Path, "{"+p.Name+"}", fmt.Sprint(example))
}
}
if p.Location == "query" {
if example, ok := p.Extensions["x-example"]; ok {
q.Set(p.Name, fmt.Sprint(example))
}
}
}
u.RawQuery = q.Encode()
return u.String()
}
df["body"] = func(parameters generator.GenParameters) interface{} {
for _, p := range parameters {
if p.Location == "body" {
if example, ok := p.Extensions["x-example"]; ok {
return example
}
}
}
return nil
}
df["ginizePath"] = func(path string) string {
return strings.Replace(strings.Replace(path, "{", ":", -1), "}", "", -1)
}
df["export"] = func(name string) string {
return strings.ToUpper(name[0:1]) + name[1:]
}
df["basePaths"] = func(operations []generator.GenOperation) []string {
var l []string
var seen = map[string]bool{}
for _, operation := range operations {
if _, ok := seen[operation.BasePath]; !ok {
l = append(l, strings.TrimPrefix(operation.BasePath, "/"))
seen[operation.BasePath] = true
}
}
return l
}
df["roles"] = func(reqs []analysis.SecurityRequirement) string {
for _, req := range reqs {
if req.Name == "roles" {
var roles []string
for _, scope := range req.Scopes {
roles = append(roles, "role."+strcase.ToCamel(strings.ReplaceAll(scope, ":", "_")))
// roles = append(roles, permission.FromString(scope))
}
return strings.Join(roles, ", ")
}
}
return ""
}
return df
}
opts := &generator.GenOpts{
Spec: "generated/community.yml",
Target: "generated",
APIPackage: "operations",
ModelPackage: "models",
ServerPackage: "restapi",
ClientPackage: "client",
DefaultScheme: "http",
IncludeModel: true,
IncludeValidator: true,
IncludeHandler: true,
IncludeParameters: true,
IncludeResponses: true,
IncludeURLBuilder: true,
IncludeMain: true,
IncludeSupport: true,
ValidateSpec: true,
FlattenOpts: &analysis.FlattenOpts{
Minimal: true,
Verbose: true,
},
Name: "catalyst-test",
FlagStrategy: "go-flags",
CompatibilityMode: "modern",
Sections: generator.SectionOpts{
Application: []generator.TemplateOpts{
{
Name: "api-server-test",
Source: path.Join(p, "templates/api_server_test.gotmpl"),
Target: "{{ .Target }}/test",
FileName: "api_server_test.go",
},
// {
// Name: "configure",
// Source: "generator/config.gotmpl",
// Target: "{{ joinFilePath .Target .ServerPackage }}",
// FileName: "config.go",
// SkipExists: false,
// SkipFormat: false,
// },
{
Name: "embedded_spec",
Source: "asset:swaggerJsonEmbed",
Target: "{{ joinFilePath .Target .ServerPackage }}",
FileName: "embedded_spec.go",
},
{
Name: "server",
Source: path.Join(p, "templates/api.gotmpl"),
Target: "{{ joinFilePath .Target .ServerPackage }}",
FileName: "api.go",
},
{
Name: "response.go",
Source: path.Join(p, "templates/response.gotmpl"),
Target: "{{ .Target }}/restapi/api",
FileName: "response.go",
},
},
Operations: []generator.TemplateOpts{
{
Name: "parameters",
Source: path.Join(p, "templates/parameter.gotmpl"),
Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}",
FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go",
},
},
Models: []generator.TemplateOpts{
{
Name: "definition",
Source: "asset:model",
Target: "{{ joinFilePath .Target .ModelPackage }}/old",
FileName: "{{ (snakize (pascalize .Name)) }}.go",
},
// {
// Name: "model",
// Source: "generator/model.gotmpl",
// Target: "{{ joinFilePath .Target .ModelPackage }}/old2",
// FileName: "{{ (snakize (pascalize .Name)) }}.go",
// },
},
},
}
err = opts.EnsureDefaults()
if err != nil {
log.Fatalln(err)
}
err = generator.GenerateServer("catalyst", nil, nil, opts)
if err != nil {
log.Fatalln(err)
}
// loads.Spec()
// swagger.
}

61
generator/swagger.go Normal file
View File

@@ -0,0 +1,61 @@
package main
type Swagger struct {
Swagger string `yaml:"swagger" json:"swagger"`
Info Info `yaml:"info" json:"info"`
Paths map[string]PathItem `yaml:"paths" json:"paths"`
Definitions map[string]Definition `yaml:"definitions" json:"definitions"`
}
type Info struct {
Version string `yaml:"version" json:"version"`
Title string `yaml:"title" json:"title"`
}
type PathItem struct {
Get Operation `yaml:"get" json:"get"`
Post Operation `yaml:"post" json:"post"`
}
type Operation struct {
Tags []string `yaml:"tags" json:"tags"`
Summary string `yaml:"summary" json:"summary"`
Description string `yaml:"description" json:"description"`
OperationID string `yaml:"operationId" json:"operationId"`
Parameters []Parameter `yaml:"parameters" json:"parameters"`
Responses map[string]Response `yaml:"responses" json:"responses"`
}
type Parameter struct {
// Description string `yaml:"description" json:"description"`
Name string `yaml:"name" json:"name"`
In string `yaml:"in" json:"in"`
Required bool `yaml:"required" json:"required"`
Schema Schema `yaml:"schema" json:"schema"`
Direct Schema `yaml:"-,inline" json:"-,inline"`
}
type Schema struct {
Ref string `yaml:"$ref,omitempty" json:"$ref,omitempty"`
Format string `yaml:"format,omitempty" json:"format,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Description string `yaml:"description" json:"description,omitempty"`
Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
Maximum interface{} `yaml:"maximum,omitempty" json:"maximum,omitempty"`
Items *Schema `yaml:"items,omitempty" json:"items,omitempty"`
Type string `yaml:"type" json:"type,omitempty"`
AdditionalProperties *Schema `yaml:"additionalProperties,omitempty" json:"additionalProperties,omitempty"`
Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"`
}
type Response struct {
Description string `yaml:"description" json:"description"`
Schema Schema `yaml:"schema" json:"schema"`
}
type Definition struct {
Type string `yaml:"type" json:"type"`
Required []string `yaml:"required,omitempty" json:"required,omitempty"`
Embed string `yaml:"x-embed" json:"x-embed"`
Properties map[string]Schema `yaml:"properties" json:"properties"`
}

View File

@@ -0,0 +1,124 @@
package {{ .APIPackage }}
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"strings"
"golang.org/x/oauth2"
"github.com/gin-gonic/gin"
{{range .DefaultImports}}{{printf "%q" .}}
{{end}}
{{range $key, $value := .Imports}}{{$key}} {{ printf "%q" $value}}
{{end}}
"github.com/SecurityBrewery/catalyst/generated/restapi/api"
"github.com/SecurityBrewery/catalyst/role"
)
// Service is the interface that must be implemented in order to provide
// business logic for the Server service.
type Service interface {
{{range .Operations}}{{ pascalize .Name }}(ctx context.Context{{ if .Params }}, params *{{.Package}}.{{ pascalize .Name }}Params{{ end }}) *api.Response
{{end}}
}
// Config defines the config options for the API server.
type Config struct {
Address string
InsecureHTTP bool
TLSCertFile string
TLSKeyFile string
}
// Server defines the Server service.
type Server struct {
*gin.Engine
config *Config
server *http.Server
service Service
{{ range .Operations | basePaths }}{{ . | export }}Group *gin.RouterGroup
{{end}}
RoleAuth func([]role.Role) gin.HandlerFunc
}
// New initializes a new Server service.
func New(svc Service, config *Config) *Server {
engine := gin.New()
engine.Use(gin.Recovery())
return &Server{
Engine: engine,
service: svc,
config: config,
server: &http.Server{
Addr: config.Address,
Handler: engine,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
{{range .Operations | basePaths }}{{ . | export }}Group: engine.Group("/{{.}}"),
{{end}}
RoleAuth: func(i []role.Role) gin.HandlerFunc { return func(c *gin.Context) { c.Next() } },
}
}
// ConfigureRoutes configures the routes for the Server service.
// Configuring of routes includes setting up Auth if it is enabled.
func (s *Server) ConfigureRoutes() {
{{range .Operations}}s.{{ slice .BasePath 1 | export }}Group.{{.Method}}({{ .Path | ginizePath | printf "%q" }}, s.RoleAuth([]role.Role{ {{ .SecurityRequirements | roles }} }), {{.Package}}.{{ pascalize .Name }}Endpoint(s.service.{{ pascalize .Name }}))
{{end}}}
// run the Server. It will listen on either HTTP or HTTPS depending on the
// config passed to NewServer.
func (s *Server) run() error {
log.Printf("Serving on address %s\n", s.server.Addr)
if s.config.InsecureHTTP {
return s.server.ListenAndServe()
}
return s.server.ListenAndServeTLS(s.config.TLSCertFile, s.config.TLSKeyFile)
}
// Shutdown will gracefully shutdown the Server.
func (s *Server) Shutdown() error {
return s.server.Shutdown(context.Background())
}
// RunWithSigHandler runs the Server with SIGTERM handling automatically
// enabled. The server will listen for a SIGTERM signal and gracefully shutdown
// the web server.
// It's possible to optionally pass any number shutdown functions which will
// execute one by one after the webserver has been shutdown successfully.
func (s *Server) RunWithSigHandler(shutdown ...func() error) error {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
s.Shutdown()
}()
err := s.run()
if err != nil {
if err != http.ErrServerClosed {
return err
}
}
for _, fn := range shutdown {
err := fn()
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,184 @@
package test
import (
"bytes"
"context"
"encoding/json"
"github.com/SecurityBrewery/catalyst/database"
"github.com/go-openapi/swag"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/models"
"github.com/SecurityBrewery/catalyst/test"
)
func TestService(t *testing.T) {
gin.SetMode(gin.TestMode)
type args struct {
method string
url string
data interface{}
}
type want struct {
status int
body interface{}
}
tests := []struct {
name string
args args
want want
}{
{{range .Operations}}
{
name: "{{ pascalize .Name }}",
args: args{method: "{{ .Method }}", url: {{ path .BasePath .Path .Params | printf "%#v" }}{{ if .Params | body }}, data: {{ .Params | body | printf "%#v" }}{{ end }}},
want: want{
status: {{ with index .Responses 0 }}{{ .Code }},
body: {{ if ne (len .Examples) 0 }}{{ with index .Examples 0 }}{{ .Example | printf "%#v" }}{{ end }}{{ else }}nil{{ end }}{{ end }},
},
}, {{ end }}
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, _, _, _, _, db, _, server, cleanup, err := test.Server(t)
if err != nil {
t.Fatal(err)
}
defer cleanup()
if err := test.SetupTestData(ctx, db); err != nil {
t.Fatal(err)
}
setUser := func(context *gin.Context) {
busdb.SetContext(context, test.Bob)
}
server.ApiGroup.Use(setUser)
server.ConfigureRoutes()
w := httptest.NewRecorder()
// setup request
var req *http.Request
if tt.args.data != nil {
b, err := json.Marshal(tt.args.data)
if err != nil {
t.Fatal(err)
}
req = httptest.NewRequest(tt.args.method, tt.args.url, bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(tt.args.method, tt.args.url, nil)
}
// run request
server.ServeHTTP(w, req)
result := w.Result()
// assert results
if result.StatusCode != tt.want.status {
msg, _ := io.ReadAll(result.Body)
t.Fatalf("Status got = %v, want %v: %s", result.Status, tt.want.status, msg)
}
if tt.want.status != http.StatusNoContent {
jsonEqual(t, result.Body, tt.want.body)
}
})
}
}
func jsonEqual(t *testing.T, got io.Reader, want interface{}) {
var gotObject, wantObject interface{}
// load bytes
wantBytes, err := json.Marshal(want)
if err != nil {
t.Fatal(err)
}
gotBytes, err := io.ReadAll(got)
if err != nil {
t.Fatal(err)
}
fields := []string{
"created", "modified", "logs.0.created",
"artifacts.0.enrichments.hash\\.sha1.created",
"artifacts.1.enrichments.hash\\.sha1.created",
"artifacts.2.enrichments.hash\\.sha1.created",
"playbooks.simple.tasks.input.created",
"playbooks.simple.tasks.hash.created",
"playbooks.simple.tasks.escalate.created",
"playbooks.phishing.tasks.input.created",
"playbooks.phishing.tasks.hash.created",
"playbooks.phishing.tasks.escalate.created",
"playbooks.phishing.tasks.block-ioc.created",
"playbooks.phishing.tasks.block-iocs.created",
"playbooks.phishing.tasks.block-sender.created",
"playbooks.phishing.tasks.board.created",
"playbooks.phishing.tasks.board.closed",
"playbooks.phishing.tasks.escalate.created",
"playbooks.phishing.tasks.extract-iocs.created",
"playbooks.phishing.tasks.fetch-iocs.created",
"playbooks.phishing.tasks.mail-available.created",
"playbooks.phishing.tasks.search-email-gateway.created",
"0.playbooks.phishing.tasks.block-ioc.created",
"0.playbooks.phishing.tasks.block-iocs.created",
"0.playbooks.phishing.tasks.block-sender.created",
"0.playbooks.phishing.tasks.board.created",
"0.playbooks.phishing.tasks.escalate.created",
"0.playbooks.phishing.tasks.extract-iocs.created",
"0.playbooks.phishing.tasks.fetch-iocs.created",
"0.playbooks.phishing.tasks.mail-available.created",
"0.playbooks.phishing.tasks.search-email-gateway.created",
"tickets.0.playbooks.phishing.tasks.block-ioc.created",
"tickets.0.playbooks.phishing.tasks.block-iocs.created",
"tickets.0.playbooks.phishing.tasks.block-sender.created",
"tickets.0.playbooks.phishing.tasks.board.created",
"tickets.0.playbooks.phishing.tasks.escalate.created",
"tickets.0.playbooks.phishing.tasks.extract-iocs.created",
"tickets.0.playbooks.phishing.tasks.fetch-iocs.created",
"tickets.0.playbooks.phishing.tasks.mail-available.created",
"tickets.0.playbooks.phishing.tasks.search-email-gateway.created",
"secret", "0.created", "comments.0.created",
}
for _, field := range fields {
gField := gjson.GetBytes(wantBytes, field)
if gField.Exists() && gjson.GetBytes(gotBytes, field).Exists() {
gotBytes, err = sjson.SetBytes(gotBytes, field, gField.Value())
if err != nil {
t.Fatal(err)
}
}
}
// normalize bytes
if err = json.Unmarshal(wantBytes, &wantObject); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(gotBytes, &gotObject); err != nil {
t.Fatal(string(gotBytes), err)
}
// compare
assert.Equal(t, wantObject, gotObject)
}

View File

@@ -0,0 +1,347 @@
{{ define "sliceparamvalidator"}}
{{ if or .MinItems .MaxItems }}
{{ camelize .Name }}Size := int64(len({{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{ .ValueExpression }}))
{{ end }}
{{ if .MinItems }}
if err := validate.MinItems({{ .Path }}, {{ printf "%q" .Location }}, {{ camelize .Name }}Size, {{ .MinItems }}); err != nil {
return err
}
{{ end }}
{{ if .MaxItems }}
if err := validate.MaxItems({{ .Path }}, {{ printf "%q" .Location }}, {{ camelize .Name }}Size, {{.MaxItems}}); err != nil {
return err
}
{{ end }}
{{ if .UniqueItems }}
if err := validate.UniqueItems({{ .Path }}, {{ printf "%q" .Location }}, {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{ .ValueExpression }}); err != nil {
return err
}
{{ end }}
{{ if .Enum }}
if err := validate.Enum({{ .Path }}, {{ printf "%q" .Location }}, {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{ .ValueExpression }}, {{ .Enum }}); err != nil {
return err
}
{{ end }}
{{ end }}
{{ define "customValidationPrimitive" }}
{{if .MinLength}}
if err := validate.MinLength({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if .IsNullable }}(*{{ end }}{{.ValueExpression}}{{ if .IsNullable }}){{ end }}{{ if .IsCustomFormatter }}.String(){{ end }}, {{.MinLength}}); err != nil {
return err
}
{{end}}
{{if .MaxLength}}
if err := validate.MaxLength({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if .IsNullable }}(*{{ end }}{{.ValueExpression}}{{ if .IsNullable }}){{ end }}{{ if .IsCustomFormatter }}.String(){{ end }}, {{.MaxLength}}); err != nil {
return err
}
{{end}}
{{if .Pattern}}
if err := validate.Pattern({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if .IsNullable }}(*{{ end }}{{.ValueExpression}}{{ if .IsNullable }}){{ end }}{{ if .IsCustomFormatter }}.String(){{ end }}, `{{.Pattern}}`); err != nil {
return err
}
{{end}}
{{if .Minimum}}
if err := validate.Minimum{{ if eq .SwaggerType "integer" }}Int{{ end }}({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if eq .SwaggerType "integer" }}int{{ else }}float{{ end }}64({{ if .IsNullable }}*{{ end }}{{.ValueExpression}}), {{.Minimum}}, {{.ExclusiveMinimum}}); err != nil {
return err
}
{{end}}
{{if .Maximum}}
if err := validate.Maximum{{ if eq .SwaggerType "integer" }}Int{{ end }}({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if eq .SwaggerType "integer" }}int{{ else }}float{{ end }}64({{ if .IsNullable }}*{{ end }}{{.ValueExpression}}), {{.Maximum}}, {{.ExclusiveMaximum}}); err != nil {
return err
}
{{end}}
{{if .MultipleOf}}
if err := validate.MultipleOf({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, float64({{ if .IsNullable }}*{{ end }}{{.ValueExpression}}), {{.MultipleOf}}); err != nil {
return err
}
{{end}}
{{if .Enum}}
if err := validate.Enum({{ if .Path }}{{ .Path }}{{else}}""{{end}}, {{ printf "%q" .Location }}, {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) .IsNullable }}*{{ end }}{{.ValueExpression}}{{ if .IsCustomFormatter }}.String(){{ end }}, {{ printf "%#v" .Enum}}); err != nil {
return err
}
{{end}}
{{end}}
{{ define "propertyparamvalidator" }}
{{ if .IsPrimitive }}{{ template "customValidationPrimitive" . }}{{ end }}
{{ if .IsCustomFormatter }}
if err := validate.FormatOf({{.Path}}, "{{.Location}}", "{{.SwaggerFormat}}", {{.ValueExpression}}.String(), formats); err != nil {
return err
}{{ end }}
{{ if .IsArray }}{{ template "sliceparamvalidator" . }}{{ end -}}
{{ end }}
{{define "bindprimitiveparam" }}
{{ end }}
{{ define "sliceparambinder" }}
var {{ varname .Child.ValueExpression }}R {{ .GoType }}
for {{ if or .Child.HasValidations .Child.Converter .Child.IsCustomFormatter }}{{ .IndexVar }}{{ else }}_{{ end }}, {{ varname .Child.ValueExpression }}V := range {{ varname .Child.ValueExpression }}C {
{{ if or .Child.IsArray -}}
{{ .Child.Child.ValueExpression }}C := swag.SplitByFormat({{ varname .Child.ValueExpression }}V, {{ printf "%q" .Child.CollectionFormat }})
{{ template "sliceparambinder" .Child }}
{{- else -}}
{{ if .Child.Converter -}}
{{ varname .Child.ValueExpression }}, err := {{ .Child.Converter }}({{ varname .Child.ValueExpression }}V)
if err != nil {
return errors.InvalidType({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, "{{ .Child.GoType }}", {{ varname .Child.ValueExpression }})
}
{{- else if .Child.IsCustomFormatter -}}
{{ varname .Child.ValueExpression }}, err := formats.Parse({{ varname .Child.ValueExpression }}V)
if err != nil {
return errors.InvalidType({{ .Child.Path }}, {{ printf "%q" .Child.Location }}, "{{ .Child.GoType }}", {{ varname .Child.ValueExpression }})
}
{{- else -}}
{{ varname .Child.ValueExpression }} := {{ varname .Child.ValueExpression }}V
{{ end }}
{{- end }}
{{ template "propertyparamvalidator" .Child }}
{{ varname .Child.ValueExpression }}R = append({{ varname .Child.ValueExpression }}R, {{ varname .Child.ValueExpression }})
}
{{ end }}
package {{ .Package }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-openapi/errors"
"github.com/go-openapi/validate"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
strfmt "github.com/go-openapi/strfmt"
{{ range .DefaultImports }}{{ printf "%q" .}}
{{ end }}
{{ range $key, $value := .Imports }}{{ $key }} {{ printf "%q" $value }}
{{ end }}
"github.com/SecurityBrewery/catalyst/generated/restapi/api"
)
// {{ pascalize .Name }}Endpoint executes the core logic of the related
// route endpoint.
func {{ pascalize .Name }}Endpoint(handler func(ctx context.Context{{ if .Params }}, params *{{ pascalize .Name }}Params{{ end }}) *api.Response) gin.HandlerFunc {
return func (ctx *gin.Context) {
{{ if .Params }}// generate params from request
params := New{{ pascalize .Name }}Params()
err := params.ReadRequest(ctx)
if err != nil {
errObj := err.(*errors.CompositeError)
ctx.Writer.Header().Set("Content-Type", "application/problem+json")
ctx.JSON(int(errObj.Code()), gin.H{"error": errObj.Error()})
return
}
{{ end }}
resp := handler(ctx{{ if .Params }}, params{{end}})
switch resp.Code {
case http.StatusNoContent:
ctx.AbortWithStatus(resp.Code)
default:
ctx.JSON(resp.Code, resp.Body)
}
}
}
// New{{ pascalize .Name }}Params creates a new {{ pascalize .Name }}Params object
// with the default values initialized.
func New{{ pascalize .Name }}Params() *{{ pascalize .Name }}Params {
var (
{{ range .Params }}{{ if .HasDefault }}{{ if not .IsFileParam }}{{ varname .ID}}Default = {{ if .IsPrimitive}}{{.GoType}}({{ end}}{{ printf "%#v" .Default }}{{ if .IsPrimitive }}){{ end }}
{{ end }}{{ end }}{{end}}
)
return &{{ pascalize .Name }}Params{ {{ range .Params }}{{ if .HasDefault }}
{{ pascalize .ID}}: {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}&{{ end }}{{ varname .ID }}Default,
{{ end }}{{ end }} }
}
// {{ pascalize .Name }}Params contains all the bound params for the {{ humanize .Name }} operation
// typically these are obtained from a http.Request
//
// swagger:parameters {{ .Name }}
type {{ pascalize .Name }}Params struct {
{{ range .Params }}/*{{ if .Description }}{{ .Description }}{{ end }}{{ if .Required }}
Required: true{{ end }}{{ if .Maximum }}
Maximum: {{ if .ExclusiveMaximum }}< {{ end }}{{ .Maximum }}{{ end }}{{ if .Minimum }}
Minimum: {{ if .ExclusiveMinimum }}> {{ end }}{{ .Minimum }}{{ end }}{{ if .MultipleOf }}
Multiple Of: {{ .MultipleOf }}{{ end }}{{ if .MaxLength }}
Max Length: {{ .MaxLength }}{{ end }}{{ if .MinLength }}
Min Length: {{ .MinLength }}{{ end }}{{ if .Pattern }}
Pattern: {{ .Pattern }}{{ end }}{{ if .MaxItems }}
Max Items: {{ .MaxItems }}{{ end }}{{ if .MinItems }}
Min Items: {{ .MinItems }}{{ end }}{{ if .UniqueItems }}
Unique: true{{ end }}{{ if .Location }}
In: {{ .Location }}{{ end }}{{ if .CollectionFormat }}
Collection Format: {{ .CollectionFormat }}{{ end }}{{ if .HasDefault }}
Default: {{ printf "%#v" .Default }}{{ end }}
*/
{{ if not .Schema }}{{ pascalize .ID }} {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsFileParam) (not .IsStream) .IsNullable }}*{{ end }}{{.GoType}}{{ else }}{{ pascalize .Name }} {{ if and (not .Schema.IsBaseType) .IsNullable (not .Schema.IsStream) }}*{{ end }}{{.GoType}}{{ end }}
{{ end}}
}
// ReadRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) ReadRequest(ctx *gin.Context) error {
var res []error
{{ if .HasQueryParams }}qs := runtime.Values(ctx.Request.URL.Query()){{ end }}
{{ if .HasFormParams }}if err := ctx.Request.ParseMultipartForm(32 << 20); err != nil {
if err != http.ErrNotMultipart {
return err
} else if err := ctx.Request.ParseForm(); err != nil {
return err
}
}{{ if .HasFormValueParams }}
fds := runtime.Values(ctx.Request.Form)
{{ end }}{{ end }}
{{ range .Params }}
{{ if not .IsArray }}{{ if .IsQueryParam }}q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, _ := qs.GetOK({{ .Path }})
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(q{{ pascalize .Name }}, qhk{{ pascalize .Name }}); err != nil {
res = append(res, err)
}
{{ else if .IsPathParam }}r{{ pascalize .Name }} := []string{ctx.Param({{ .Path }})}
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(r{{ pascalize .Name }}, true); err != nil {
res = append(res, err)
}
{{ else if .IsHeaderParam }}if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(ctx.Request.Header[http.CanonicalHeaderKey({{ .Path }})], true); err != nil {
res = append(res, err)
}
{{ else if .IsFormParam }}{{if .IsFileParam }}{{ camelize .Name }}, {{ camelize .Name }}Header, err := ctx.Request.FormFile({{ .Path }})
if err != nil {
res = append(res, errors.New(400, "reading file %q failed: %v", {{ printf "%q" (camelize .Name) }}, err))
} else {
{{ .ReceiverName }}.{{ pascalize .Name }} = &runtime.File{Data: {{ camelize .Name }}, Header: {{ camelize .Name }}Header}
}
{{ else }}fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, _ := fds.GetOK({{ .Path }})
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}); err != nil {
res = append(res, err)
}
{{ end }}{{ end }}
{{ else if .IsArray }}{{ if .IsQueryParam }}q{{ pascalize .Name }}, qhk{{ pascalize .Name }}, _ := qs.GetOK({{ .Path }})
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(q{{ pascalize .Name }}, qhk{{ pascalize .Name }}); err != nil {
res = append(res, err)
}
{{ else if and .IsFormParam }}fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}, _ := fds.GetOK({{ .Path }})
if err := {{ .ReceiverName }}.bind{{ pascalize .ID }}(fd{{ pascalize .Name }}, fdhk{{ pascalize .Name }}); err != nil {
res = append(res, err)
}
{{ end }}{{ end }}
{{ if and .IsBodyParam .Schema }}if runtime.HasBody(ctx.Request) {
{{ if .Schema.IsStream }}{{ .ReceiverName }}.{{ pascalize .Name }} = ctx.Request.Body
{{ else }}{{ if and .Schema.IsBaseType .Schema.IsExported }}body, err := {{ .ModelsPackage }}.Unmarshal{{ dropPackage .GoType }}{{ if .IsArray }}Slice{{ end }}(ctx.Request.Body, route.Consumer)
if err != nil { {{ if .Required }}
if err == io.EOF {
err = errors.Required({{ .Path }}, {{ printf "%q" .Location }}, "")
}
{{ end }}res = append(res, err)
{{ else }}var body {{ .GoType }}
if err := ctx.BindJSON(&body); err != nil { {{ if .Required }}
if err == io.EOF {
res = append(res, errors.Required({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, ""))
} else { {{ end }}
res = append(res, errors.NewParseError({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, "", err)){{ if .Required }}
}
{{ end }}
{{ end }}} else {
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ if and (not .Schema.IsBaseType) .IsNullable }}&{{ end }}body
}{{ end }}
}{{ if .Required }} else {
res = append(res, errors.Required({{ printf "%q" (camelize .Name) }}, {{ printf "%q" .Location }}, ""))
} {{ end }}
{{ end }}
{{ end }}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
{{ $className := (pascalize .Name) }}
{{ range .Params }}
{{ if not (or .IsBodyParam .IsFileParam) }}
{{ if or .IsPrimitive .IsCustomFormatter }}
func ({{ .ReceiverName }} *{{ $className }}Params) bind{{ pascalize .ID }}(rawData []string, hasKey bool) error {
{{ if and (not .IsPathParam) .Required }}if !hasKey {
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, rawData)
}
{{ end }}var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
{{ if and (not .IsPathParam) .Required (not .AllowEmptyValue) }}if err := validate.RequiredString({{ .Path }}, {{ printf "%q" .Location }}, raw); err != nil {
return err
}
{{ else if and ( not .IsPathParam ) (or (not .Required) .AllowEmptyValue) }}if raw == "" { // empty values pass all other validations
{{ if .HasDefault }}var {{ camelize .Name}}Default {{ if not .IsFileParam }}{{ .GoType }}{{ else }}os.File{{end}} = {{ if .IsPrimitive}}{{.GoType}}({{ end}}{{ printf "%#v" .Default }}{{ if .IsPrimitive }}){{ end }}
{{ .ValueExpression }} = {{ if and (not .IsArray) (not .HasDiscriminator) (or .IsNullable ) (not .IsStream) }}&{{ end }}{{ camelize .Name }}Default
{{ end }}return nil
}
{{ end }}
{{ if .Converter }}value, err := {{ .Converter }}(raw)
if err != nil {
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, raw)
}
{{ .ValueExpression }} = {{ if .IsNullable }}&{{ end }}value
{{ else if .IsCustomFormatter }}value, err := formats.Parse({{ printf "%q" .SwaggerFormat }}, raw)
if err != nil {
return errors.InvalidType({{ .Path }}, {{ printf "%q" .Location }}, {{ printf "%q" .GoType }}, raw)
}
{{ .ValueExpression }} = {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsFileParam) (not .IsStream) (not .IsNullable) }}*{{ end }}(value.(*{{ .GoType }}))
{{else}}{{ .ValueExpression }} = {{ if .IsNullable }}&{{ end }}raw
{{ end }}
{{if .HasValidations }}if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(); err != nil {
return err
}
{{ end }}
return nil
}
{{else if .IsArray}}
func ({{ .ReceiverName }} *{{ $className }}Params) bind{{ pascalize .ID }}(rawData []string, hasKey bool) error {
{{if .Required }}if !hasKey {
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, rawData)
}
{{ end }}
{{ if eq .CollectionFormat "multi" }}{{ varname .Child.ValueExpression }}C := rawData{{ else }}var qv{{ pascalize .Name }} string
if len(rawData) > 0 {
qv{{ pascalize .Name }} = rawData[len(rawData) - 1]
}
{{ varname .Child.ValueExpression }}C := swag.SplitByFormat(qv{{ pascalize .Name }}, {{ printf "%q" .CollectionFormat }}){{ end }}
{{if and .Required (not .AllowEmptyValue) }}
if len({{ varname .Child.ValueExpression }}C) == 0 {
return errors.Required({{ .Path }}, {{ printf "%q" .Location }}, {{ varname .Child.ValueExpression }}C)
}
{{ end }}
{{ if not .Required }}{{ if .HasDefault }}defValue := swag.SplitByFormat({{ .Default }}, {{ printf "%q" .CollectionFormat }})
if len({{ varname .Child.ValueExpression }}C) == 0 && len(defValue) > 0 {
{{ .ValueExpression }} = defValue
{{ else }}if len({{ varname .Child.ValueExpression }}C) == 0 {
return nil{{ end }}
}{{ end }}
{{ template "sliceparambinder" . }}
{{ .ValueExpression }} = {{ varname .Child.ValueExpression }}R
{{ if .HasSliceValidations }}if err := {{ .ReceiverName }}.validate{{ pascalize .ID }}(); err != nil {
return err
}
{{ end }}
return nil
}
{{ end }}
{{ if or .HasValidations .HasSliceValidations }}
func ({{ .ReceiverName }} *{{ $className }}Params) validate{{ pascalize .ID }}() error {
{{ template "propertyparamvalidator" . }}
return nil
}
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,6 @@
package api
type Response struct {
Code int
Body interface{}
}

View File

@@ -0,0 +1,64 @@
{{- /*gotype: github.com/SecurityBrewery/catalyst/generator.Swagger */ -}}
package models
import (
"fmt"
"strings"
"time"
"github.com/xeipuuv/gojsonschema"
)
var (
schemaLoader = gojsonschema.NewSchemaLoader()
{{ range $index, $element := .Definitions }}{{ $index }}Schema = new(gojsonschema.Schema)
{{ end }})
func init() {
err := schemaLoader.AddSchemas(
{{ range $index, $element := .Definitions }}gojsonschema.NewStringLoader(`{{ tojson $index $element }}`),
{{ end }}
)
if err != nil {
panic(err)
}
{{ range $index, $element := .Definitions }}{{ $index }}Schema = mustCompile(`#/definitions/{{ $index }}`)
{{ end }}}
{{ range $index, $element := .Definitions }}
type {{ $index }} struct {
{{ range $pindex, $pelement := .Properties }} {{ camel $pindex }} {{ gotype $pindex $pelement $element.Required }} `json:"{{ $pindex }}{{ if omitempty $pindex $element.Required }},omitempty{{ end }}"`
{{ end }}}
{{ end }}
func mustCompile(uri string) *gojsonschema.Schema {
s, err := schemaLoader.Compile(gojsonschema.NewReferenceLoader(uri))
if err != nil {
panic(err)
}
return s
}
func validate(s *gojsonschema.Schema, b []byte) error {
res, err := s.Validate(gojsonschema.NewStringLoader(string(b)))
if err != nil {
return err
}
if len(res.Errors()) > 0 {
var l []string
for _, e := range res.Errors() {
l = append(l, e.String())
}
return fmt.Errorf("validation failed: %v", strings.Join(l, ", "))
}
return nil
}
const (
{{ range $index, $element := .Definitions }}{{ range $pindex, $pelement := .Properties }}{{ range $eindex, $eelement := .Enum }}
{{ $index | camel }}{{ $pindex | camel }}{{ $eelement | camel }} = "{{ $eelement }}"
{{ end }}{{ end }}{{ end }}
)