├── cmd ├── elegen │ ├── templates │ │ ├── README.md │ │ └── identities_registry.gotpl │ ├── versions │ │ ├── doc.go │ │ └── versions.go │ ├── main.go │ └── writers.go └── internal │ └── genopenapi3 │ ├── exports_test.go │ ├── consts_test.go │ ├── converter_model_root_test.go │ ├── exports.go │ ├── gen_params.go │ ├── converter_errors_test.go │ ├── gen_model.go │ ├── converter_helpers_test.go │ ├── gen_converter.go │ ├── gen_relations.go │ └── converter_relations_spec_root_test.go ├── .gitignore ├── matcher_options.go ├── Makefile ├── .goreleaser.yml ├── .github └── workflows │ └── build-go.yaml ├── test ├── gen.sh └── model │ ├── unmarshalable.go │ ├── identities_registry.go │ ├── root.go │ └── relationships_registry.go ├── filterparser_options.go ├── README.md ├── matcher_errors.go ├── response.go ├── atomicjob.go ├── operations.go ├── .golangci.yaml ├── response_test.go ├── doc.go ├── operations_test.go ├── manager.go ├── identity_test.go ├── go.mod ├── internal └── attribute_specifiable.go ├── relationships.go ├── namespace.go ├── filterscanner.go ├── utils.go ├── atomicjob_test.go ├── event.go ├── error.go ├── verify.go ├── identity.go ├── push_config.go ├── attribute_test.go ├── attribute.go ├── encoding.go ├── event_test.go └── LICENSE /cmd/elegen/templates/README.md: -------------------------------------------------------------------------------- 1 | # Templates 2 | 3 | These file need to be embedded in the binary! 4 | 5 | make package 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .tags 3 | *.pyc 4 | *.egg-info 5 | */debug 6 | codegen 7 | vendor 8 | .idea 9 | cmd/elegen/elegen 10 | cmd/elegen/build 11 | *.lock 12 | unit_coverage.out 13 | cov.report 14 | artifacts 15 | profile.out 16 | dist 17 | coverage.xml 18 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/exports_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import "testing" 4 | 5 | func TestGeneratorFunc(t *testing.T) { 6 | t.Parallel() 7 | 8 | t.Skip("TODO--The GeneratorFunc adapter is likely to change; so let's spare testing for later") 9 | } 10 | -------------------------------------------------------------------------------- /matcher_options.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | type matchConfig struct{} 4 | 5 | // MatcherOption represents the type for the options that can be passed to the helper `MatchesFilter` which can be used 6 | // to alter the matching behaviour 7 | type MatcherOption func(*matchConfig) 8 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/consts_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | const regolitheINI = ` 4 | [regolithe] 5 | product_name = dummy 6 | 7 | [transformer] 8 | name = gaia 9 | url = go.aporeto.io/api 10 | author = Aporeto Inc. 11 | email = dev@aporeto.com 12 | version = 1.0 13 | ` 14 | 15 | const typeMapping = ` 16 | '[]byte': 17 | openapi3: 18 | type: |- 19 | { 20 | "type": "string" 21 | } 22 | ` 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --warn-undefined-variables 2 | SHELL := /bin/bash -o pipefail 3 | 4 | export GO111MODULE = on 5 | 6 | default: lint test 7 | 8 | lint: 9 | golangci-lint run ./... 10 | 11 | sec: 12 | gosec -quiet ./... 13 | 14 | .PHONY: test 15 | test: 16 | go test ./... -race -cover -covermode=atomic -coverprofile=unit_coverage.out 17 | 18 | update-deps: 19 | go get -u go.aporeto.io/regolithe@master 20 | go get -u github.com/smartystreets/goconvey@latest 21 | go get -u go.uber.org/zap@latest 22 | go get -u golang.org/x/sync@latest 23 | go get -u golang.org/x/text@latest 24 | go get -u golang.org/x/tools@latest 25 | 26 | go mod tidy 27 | -------------------------------------------------------------------------------- /cmd/elegen/versions/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package versions 13 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | snapshot: 2 | name_template: "{{ .Tag }}-next" 3 | changelog: 4 | sort: asc 5 | filters: 6 | exclude: 7 | - '^docs:' 8 | - '^test:' 9 | - '^examples:' 10 | builds: 11 | - id: elegen 12 | main: ./cmd/elegen 13 | binary: elegen 14 | goos: 15 | - linux 16 | - freebsd 17 | - darwin 18 | goarch: 19 | - amd64 20 | env: 21 | - CGO_ENABLED=0 22 | 23 | archives: 24 | - id: elegen 25 | format: binary 26 | builds: 27 | - elegen 28 | 29 | signs: 30 | - artifacts: checksum 31 | args: ["-u", "0C3214A61024881F5CA1F5F056EDB08A11DCE325", "--output", "${signature}", "--detach-sign", "${artifact}"] 32 | -------------------------------------------------------------------------------- /.github/workflows/build-go.yaml: -------------------------------------------------------------------------------- 1 | name: build-go 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | env: 13 | GO111MODULE: on 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | go: 22 | - "1.24" 23 | steps: 24 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 25 | 26 | - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4 27 | with: 28 | go-version: ${{ matrix.go }} 29 | cache: true 30 | 31 | - name: setup 32 | run: | 33 | go install github.com/securego/gosec/cmd/gosec@latest 34 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest 35 | golangci-lint version 36 | 37 | - name: build 38 | run: | 39 | make 40 | -------------------------------------------------------------------------------- /test/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1 4 | 5 | /Users/knainwal/apomux/workspace/code/go/src/go.aporeto.io/elemental/cmd/elegen/elegen folder -d ../../regolithe/spec/tests || exit 1 6 | 7 | mkdir -p model 8 | mv codegen/elemental/* ./model 9 | rm -rf codegen 10 | 11 | cat <../data_test.go 12 | package elemental 13 | 14 | import ( 15 | "fmt" 16 | "time" 17 | 18 | "go.mongodb.org/mongo-driver/bson" 19 | "go.mongodb.org/mongo-driver/bson/primitive" 20 | "github.com/mitchellh/copystructure" 21 | ) 22 | 23 | //lint:file-ignore U1000 auto generated code. 24 | EOF 25 | { 26 | tail -n +15 model/list.go 27 | tail -n +14 model/task.go 28 | tail -n +20 model/unmarshalable.go 29 | tail -n +14 model/user.go 30 | tail -n +21 model/root.go 31 | tail -n +7 model/identities_registry.go 32 | tail -n +7 model/relationships_registry.go 33 | } >>../data_test.go 34 | 35 | sed 's/elemental\.//g' ../data_test.go >../data_test.go.new 36 | mv ../data_test.go.new ../data_test.go 37 | rm -f ../data_test.go.new 38 | -------------------------------------------------------------------------------- /filterparser_options.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | import "strings" 4 | 5 | type filterParserConfig struct { 6 | // map to support an O(1) lookup during parsing 7 | unsupportedComparators map[parserToken]struct{} 8 | } 9 | 10 | // FilterParserOption represents the type for the options that can be passed to `NewFilterParser` which can be used to 11 | // alter parsing behaviour 12 | type FilterParserOption func(*filterParserConfig) 13 | 14 | // OptUnsupportedComparators accepts a slice of comparators that will limit the set of comparators that the parser will accept. 15 | // If supplied, the parser will return an error if the filter being parsed contains a comparator provided in the blacklist. 16 | func OptUnsupportedComparators(blacklist []FilterComparator) FilterParserOption { 17 | return func(config *filterParserConfig) { 18 | config.unsupportedComparators = map[parserToken]struct{}{} 19 | for _, c := range blacklist { 20 | if token, ok := operatorsToToken[strings.ToUpper(translateComparator(c))]; ok { 21 | config.unsupportedComparators[token] = struct{}{} 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/converter_model_root_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConverter_Do__model_root(t *testing.T) { 8 | t.Parallel() 9 | 10 | cases := map[string]testCase{ 11 | 12 | "should-be-ignored": { 13 | inSpec: ` 14 | model: 15 | root: true 16 | rest_name: root 17 | resource_name: root 18 | entity_name: Root 19 | package: root 20 | group: core 21 | description: root object. 22 | `, 23 | outDocs: map[string]string{ 24 | "toplevel": ` 25 | { 26 | "openapi": "3.0.3", 27 | "info": { 28 | "contact": { 29 | "email": "dev@aporeto.com", 30 | "name": "Aporeto Inc.", 31 | "url": "go.aporeto.io/api" 32 | }, 33 | "license": { 34 | "name": "TODO" 35 | }, 36 | "termsOfService": "https://localhost/TODO", 37 | "version": "1.0", 38 | "title": "toplevel" 39 | }, 40 | "components": {}, 41 | "paths": {} 42 | } 43 | `, 44 | }, 45 | }, 46 | } 47 | runAllTestCases(t, cases) 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elemental 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/cbd8761ba935460885a1ff9b49497621)](https://www.codacy.com/gh/PaloAltoNetworks/elemental/dashboard?utm_source=github.com&utm_medium=referral&utm_content=PaloAltoNetworks/elemental&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/cbd8761ba935460885a1ff9b49497621)](https://www.codacy.com/gh/PaloAltoNetworks/elemental/dashboard?utm_source=github.com&utm_medium=referral&utm_content=PaloAltoNetworks/elemental&utm_campaign=Badge_Coverage) 4 | 5 | > Note: This is a work in progress 6 | 7 | Elemental contains the foundations and code generator to translate and use 8 | Regolithe specifications in a Bahamut/Manipulate environment. It also provides 9 | basic definition of a generic request and response and other low level functions 10 | like optimized encoder and decoder, validations and more. 11 | 12 | The generated model from Regolithe specifications (when using elegen from 13 | cmd/elegen) will implement the interfaces described in this package to serve as a base 14 | for being used in a bahamut microservice. 15 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/exports.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | 10 | "go.aporeto.io/regolithe/spec" 11 | ) 12 | 13 | // Config is used to guide the generator function 14 | type Config struct { 15 | Public bool 16 | SplitOutput bool 17 | OutputDir string 18 | } 19 | 20 | // GeneratorFunc will convert the given spec set into an openapi3 document 21 | func GeneratorFunc(sets []spec.SpecificationSet, cfg Config) error { 22 | 23 | outFolder := path.Join(cfg.OutputDir, "openapi3") 24 | if err := os.MkdirAll(outFolder, 0750); err != nil && !os.IsExist(err) { 25 | return fmt.Errorf("'%s': error creating directory: %w", outFolder, err) 26 | } 27 | 28 | newFileFunc := func(name string) (io.WriteCloser, error) { 29 | filename := filepath.Join(outFolder, name) 30 | file, err := os.Create(filename) 31 | if err != nil { 32 | return nil, fmt.Errorf("'%s': error creating file: %w", filename, err) 33 | } 34 | return file, nil 35 | } 36 | 37 | set := sets[0] 38 | converter := newConverter(set, cfg) 39 | if err := converter.Do(newFileFunc); err != nil { 40 | return fmt.Errorf("error generating openapi3 document from spec set '%s': %w", set.Configuration().Name, err) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /matcher_errors.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // ErrUnsupportedComparator is the error type that will be returned in the event that that an unsupported comparator 9 | // is used in the filter. 10 | type ErrUnsupportedComparator struct { 11 | Err error 12 | } 13 | 14 | // Is reports whether the provided error has the same type as ErrUnsupportedComparator. This was added as part of the new 15 | // error handling APIs added to Go 1.13 16 | func (e ErrUnsupportedComparator) Is(err error) bool { 17 | return reflect.TypeOf(err) == reflect.TypeOf(e) 18 | } 19 | 20 | // Unwrap returns the embedded error in ErrUnsupportedComparator. 21 | func (e ErrUnsupportedComparator) Unwrap() error { 22 | return e.Err 23 | } 24 | 25 | func (e ErrUnsupportedComparator) Error() string { 26 | return fmt.Sprintf("unsupported comparator: %s", e.Err) 27 | } 28 | 29 | // MatcherError is the error type that will be returned by elemental.MatchesFilter in the event that it returns an error 30 | type MatcherError struct { 31 | Err error 32 | } 33 | 34 | func (me *MatcherError) Error() string { 35 | return fmt.Sprintf("elemental: unable to match: %s", me.Err) 36 | } 37 | 38 | // Unwrap returns the the error contained in 'MatcherError'. This is a special method that aids in error handling for clients 39 | // using Go 1.13 and beyond as they can now utilize the new 'Is' function added to the 'errors' package. 40 | func (me *MatcherError) Unwrap() error { 41 | return me.Err 42 | } 43 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import "net/http" 15 | 16 | // A Response contains the response from a Request. 17 | type Response struct { 18 | StatusCode int 19 | Data []byte 20 | Count int 21 | Total int 22 | Next string 23 | Messages []string 24 | Redirect string 25 | RequestID string 26 | Request *Request 27 | Cookies []*http.Cookie 28 | } 29 | 30 | // NewResponse returns a new Response 31 | func NewResponse(req *Request) *Response { 32 | 33 | return &Response{ 34 | RequestID: req.RequestID, 35 | Request: req, 36 | } 37 | } 38 | 39 | // GetEncoding returns the encoding used to encode the entity. 40 | func (r *Response) GetEncoding() EncodingType { 41 | return r.Request.Accept 42 | } 43 | 44 | // Encode encodes the given oject into the response. 45 | func (r *Response) Encode(obj any) (err error) { 46 | 47 | r.Data, err = Encode(r.GetEncoding(), obj) 48 | return err 49 | } 50 | -------------------------------------------------------------------------------- /atomicjob.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // AtomicJob takes a func() error and returns a func(context.Context) error. 9 | // The returned function can be called as many time as you like, but only 10 | // one instance of the given job can be run at the same time. 11 | // 12 | // The returned function will either execute job if it 13 | // it not already running or wait for the currently running job to finish. 14 | // In both cases, the returned error from the job will be forwareded and returned 15 | // to every caller. 16 | // 17 | // You must pass a context.Context to the returned function so you can 18 | // control how much time you are willing to wait for the job to complete. 19 | // 20 | // If you wish to change some external state from within the job function, 21 | // it is your responsibility to ensure everything is thread safe. 22 | func AtomicJob(job func() error) func(context.Context) error { 23 | 24 | var l sync.RWMutex 25 | var errorChs []chan error 26 | 27 | sem := make(chan struct{}, 1) 28 | 29 | return func(ctx context.Context) error { 30 | 31 | errCh := make(chan error) 32 | 33 | l.Lock() 34 | errorChs = append(errorChs, errCh) 35 | l.Unlock() 36 | 37 | select { 38 | case sem <- struct{}{}: 39 | 40 | go func() { 41 | 42 | err := job() 43 | 44 | l.Lock() 45 | for _, ch := range errorChs { 46 | select { 47 | case ch <- err: 48 | default: 49 | } 50 | } 51 | errorChs = nil 52 | l.Unlock() 53 | 54 | <-sem 55 | }() 56 | 57 | default: 58 | } 59 | 60 | select { 61 | case err := <-errCh: 62 | return err 63 | case <-ctx.Done(): 64 | return ctx.Err() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /operations.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "fmt" 16 | ) 17 | 18 | // Operation represents an operation to apply on an Identifiable 19 | // from a Request. 20 | type Operation string 21 | 22 | // Here are the existing Operations. 23 | const ( 24 | OperationRetrieveMany Operation = "retrieve-many" 25 | OperationRetrieve Operation = "retrieve" 26 | OperationCreate Operation = "create" 27 | OperationUpdate Operation = "update" 28 | OperationDelete Operation = "delete" 29 | OperationPatch Operation = "patch" 30 | OperationInfo Operation = "info" 31 | 32 | OperationEmpty Operation = "" 33 | ) 34 | 35 | // ParseOperation parses the given string as an Operation. 36 | func ParseOperation(op string) (Operation, error) { 37 | 38 | lop := Operation(op) 39 | 40 | if lop == OperationRetrieveMany || 41 | lop == OperationRetrieve || 42 | lop == OperationCreate || 43 | lop == OperationUpdate || 44 | lop == OperationDelete || 45 | lop == OperationPatch || 46 | lop == OperationInfo { 47 | return lop, nil 48 | } 49 | 50 | return Operation(""), fmt.Errorf("invalid operation '%s'", op) 51 | } 52 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: none 5 | 6 | enable: 7 | - errcheck 8 | - ineffassign 9 | - revive 10 | - unused 11 | - staticcheck 12 | - unused 13 | - unconvert 14 | - misspell 15 | - prealloc 16 | - nakedret 17 | - unparam 18 | 19 | settings: 20 | revive: 21 | enable-all-rules: true 22 | 23 | rules: 24 | - name: dot-imports 25 | disabled: true 26 | - name: cyclomatic 27 | disabled: true 28 | - name: cognitive-complexity 29 | disabled: true 30 | - name: empty-lines 31 | disabled: true 32 | - name: line-length-limit 33 | disabled: true 34 | - name: add-constant 35 | disabled: true 36 | - name: package-comments 37 | disabled: true 38 | - name: use-errors-new 39 | disabled: true 40 | - name: unused-receiver 41 | disabled: true 42 | - name: unused-parameter 43 | disabled: true 44 | - name: function-length 45 | disabled: true 46 | - name: flag-parameter 47 | disabled: true 48 | - name: superfluous-else 49 | disabled: true 50 | - name: unexported-naming 51 | disabled: true 52 | - name: max-public-structs 53 | disabled: true 54 | - name: nested-structs 55 | disabled: true 56 | - name: max-control-nesting 57 | disabled: true 58 | - name: bare-return 59 | disabled: true 60 | - name: unnecessary-stmt 61 | disabled: true 62 | - name: confusing-results 63 | disabled: true 64 | 65 | exclusions: 66 | paths: data_test.go 67 | 68 | formatters: 69 | default: none 70 | 71 | enable: 72 | - goimports 73 | 74 | issues: 75 | max-same-issues: 20 76 | 77 | run: 78 | timeout: 15m 79 | -------------------------------------------------------------------------------- /cmd/elegen/versions/versions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package versions 13 | 14 | // This code is autogenerated by apolibver. Do not edit manually. 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | 20 | "go.uber.org/zap/zapcore" 21 | ) 22 | 23 | // Versions of the current project. 24 | var ( 25 | ProjectVersion = "master" 26 | ProjectSha = "6f8c7be6698cfa85da84aa8dcc1e4c2815a9c8c1" 27 | ) 28 | 29 | // Versions of the various libraries used by the project. 30 | var () 31 | 32 | // Fields returns a ready to dump zap.Fields containing all the versions used. 33 | func Fields() []zapcore.Field { 34 | 35 | return []zapcore.Field{} 36 | } 37 | 38 | // Map returns the version information as a map. 39 | func Map() map[string]any { 40 | 41 | return map[string]any{ 42 | "version": ProjectVersion, 43 | "sha": ProjectSha[0:7], 44 | "libs": map[string]string{}, 45 | } 46 | } 47 | 48 | // ToString returns the string version of versions 49 | func ToString(prefix string) string { 50 | 51 | buffer := fmt.Sprintf("%s%32s : %s\n", prefix, "ProjectVersion", ProjectVersion) 52 | buffer += fmt.Sprintf("%s%32s : %s\n", prefix, "ProjectSha", ProjectSha[0:7]) 53 | buffer += fmt.Sprintf("%s%32s : %s\n", prefix, "Libraries", "") 54 | return buffer 55 | } 56 | 57 | // JSON returns the version as a ready to send json data. 58 | func JSON() ([]byte, error) { 59 | 60 | return json.MarshalIndent(Map(), "", " ") 61 | } 62 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "testing" 16 | 17 | . "github.com/smartystreets/goconvey/convey" 18 | ) 19 | 20 | func TestResponse_NewResponse(t *testing.T) { 21 | 22 | Convey("Given I create a new response", t, func() { 23 | 24 | r := NewResponse(&Request{RequestID: "x"}) 25 | 26 | Convey("Then it should be correctly initialized", func() { 27 | So(r, ShouldNotBeNil) 28 | So(r.RequestID, ShouldEqual, "x") 29 | }) 30 | }) 31 | } 32 | 33 | func TestEncode(t *testing.T) { 34 | 35 | Convey("Given I have a list and a request that Accepts JSON", t, func() { 36 | 37 | req := &Request{ 38 | Accept: EncodingTypeJSON, 39 | } 40 | resp := NewResponse(req) 41 | 42 | Convey("When I call Encode", func() { 43 | 44 | lst := NewList() 45 | err := resp.Encode(lst) 46 | 47 | Convey("Then err should be nil", func() { 48 | So(err, ShouldBeNil) 49 | }) 50 | 51 | Convey("Then it should be correctly encoded", func() { 52 | l, _ := Encode(EncodingTypeJSON, lst) 53 | So(resp.Data, ShouldResemble, l) 54 | }) 55 | }) 56 | }) 57 | 58 | Convey("Given I have a list and a request that Accepts MSGPACK", t, func() { 59 | 60 | req := &Request{ 61 | Accept: EncodingTypeMSGPACK, 62 | } 63 | resp := NewResponse(req) 64 | 65 | Convey("When I call Encode", func() { 66 | 67 | lst := NewList() 68 | err := resp.Encode(lst) 69 | 70 | Convey("Then err should be nil", func() { 71 | So(err, ShouldBeNil) 72 | }) 73 | 74 | Convey("Then it should be correctly encoded", func() { 75 | l, _ := Encode(EncodingTypeMSGPACK, lst) 76 | So(resp.Data, ShouldResemble, l) 77 | }) 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | // Package elemental provides a set of interfaces and structures used to manage a model generated from a 13 | // Regolithe Specifications Set. 14 | // 15 | // If you are not familiar with with Regolithe, please read https://github.com/aporeto-inc/regolithe. 16 | // 17 | // Elemental is the basis of Bahamut (https://github.com/aporeto-inc/bahamut) and Manipulate 18 | // (https://github.com/aporeto-inc/manipulate). 19 | // 20 | // The main interface it provides is the Identifiable. This interface must be implemented by all object of a model. 21 | // It allows to identify an object from its Identity (which is a name and category) and by its identifier. It also 22 | // embeds the Versionable interface that allows to retrieve the current version of the model. The Identifiables 23 | // interface must be implemented by lists managing a collection of Identifiable entities. 24 | // 25 | // The ModelManager is an interface to perform lookup on Identities, Relationships between them and also allow to 26 | // instantiate objects based on their Identity. 27 | // 28 | // Elemental also contains some Request/Response structures representing various Operation on Identifiable or 29 | // Identifiables as well as a bunch of validators to enforce specification constraints on attributes like max length, 30 | // pattern etc. 31 | // There is also an Event structure that can be used to notify clients of the the result of an Operation sent through 32 | // a Request. 33 | // 34 | // Elemental is mainly an abstract package and cannot really be used by itself. You must use the provided command 35 | // (elegen) to generate an Elemental Model from a Regolithe Specification Set. 36 | package elemental // import "go.aporeto.io/elemental" 37 | -------------------------------------------------------------------------------- /operations_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import "testing" 15 | 16 | func TestParseOperation(t *testing.T) { 17 | type args struct { 18 | op string 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | want Operation 24 | wantErr bool 25 | }{ 26 | { 27 | "create", 28 | args{ 29 | "create", 30 | }, 31 | OperationCreate, 32 | false, 33 | }, 34 | { 35 | "delete", 36 | args{ 37 | "delete", 38 | }, 39 | OperationDelete, 40 | false, 41 | }, 42 | { 43 | "info", 44 | args{ 45 | "info", 46 | }, 47 | OperationInfo, 48 | false, 49 | }, 50 | { 51 | "patch", 52 | args{ 53 | "patch", 54 | }, 55 | OperationPatch, 56 | false, 57 | }, 58 | { 59 | "retrieve", 60 | args{ 61 | "retrieve", 62 | }, 63 | OperationRetrieve, 64 | false, 65 | }, 66 | { 67 | "retrieve-many", 68 | args{ 69 | "retrieve-many", 70 | }, 71 | OperationRetrieveMany, 72 | false, 73 | }, 74 | { 75 | "update", 76 | args{ 77 | "update", 78 | }, 79 | OperationUpdate, 80 | false, 81 | }, 82 | { 83 | "invalid", 84 | args{ 85 | "invalid", 86 | }, 87 | OperationEmpty, 88 | true, 89 | }, 90 | { 91 | "CREATE", 92 | args{ 93 | "CREATE", 94 | }, 95 | OperationEmpty, 96 | true, 97 | }, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | got, err := ParseOperation(tt.args.op) 102 | if (err != nil) != tt.wantErr { 103 | t.Errorf("ParseOperation() error = %v, wantErr %v", err, tt.wantErr) 104 | return 105 | } 106 | if got != tt.want { 107 | t.Errorf("ParseOperation() = %v, want %v", got, tt.want) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/gen_params.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "go.aporeto.io/regolithe/spec" 8 | ) 9 | 10 | func (c *converter) convertParamDefAsQueryParams(paramDef *spec.ParameterDefinition) openapi3.Parameters { 11 | 12 | if paramDef == nil { 13 | return nil 14 | } 15 | 16 | seen := make(map[string]struct{}) 17 | 18 | params := openapi3.NewParameters() 19 | for _, e := range paramDef.Entries { 20 | 21 | if _, ok := seen[e.Name]; ok { 22 | continue 23 | } 24 | seen[e.Name] = struct{}{} 25 | 26 | p := c.convertParam(e, openapi3.ParameterInQuery) 27 | params = append(params, p) 28 | } 29 | 30 | sort.Slice(params, func(i, j int) bool { 31 | return params[i].Value.Name < params[j].Value.Name 32 | }) 33 | 34 | return params 35 | } 36 | 37 | func (*converter) convertParam(entry *spec.Parameter, in string) *openapi3.ParameterRef { 38 | 39 | param := openapi3.NewQueryParameter(entry.Name) 40 | param.Description = entry.Description 41 | param.In = in 42 | param.Example = entry.ExampleValue 43 | if param.Example == nil { 44 | param.Example = entry.DefaultValue 45 | } 46 | 47 | switch entry.Type { 48 | case spec.ParameterTypeInt: 49 | param.Schema = openapi3.NewIntegerSchema().NewRef() 50 | 51 | case spec.ParameterTypeBool: 52 | param.Schema = openapi3.NewBoolSchema().NewRef() 53 | 54 | case spec.ParameterTypeString: 55 | param.Schema = openapi3.NewStringSchema().NewRef() 56 | 57 | case spec.ParameterTypeFloat: 58 | param.Schema = openapi3.NewFloat64Schema().NewRef() 59 | 60 | case spec.ParameterTypeTime: 61 | param.Schema = openapi3.NewDateTimeSchema().NewRef() 62 | 63 | case spec.ParameterTypeDuration: 64 | param.Schema = openapi3.NewStringSchema().NewRef() // TODO: this needs to be verified 65 | 66 | case spec.ParameterTypeEnum: 67 | enumVals := make([]any, len(entry.AllowedChoices)) 68 | for i, val := range entry.AllowedChoices { 69 | enumVals[i] = val 70 | } 71 | param.Schema = openapi3.NewSchema().WithEnum(enumVals...).NewRef() 72 | 73 | default: 74 | return nil // TODO: better handling? error? 75 | } 76 | 77 | ref := &openapi3.ParameterRef{ 78 | Value: param, 79 | } 80 | 81 | return ref 82 | } 83 | 84 | func (*converter) insertParamID(params *openapi3.Parameters) { 85 | paramID := openapi3.NewPathParameter(paramNameID) 86 | paramID.Schema = openapi3.NewStringSchema().NewRef() 87 | *params = append(*params, &openapi3.ParameterRef{Value: paramID}) 88 | } 89 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | // An ModelManager is the interface that allows to search Identities 15 | // and create Identifiable and Identifiables from Identities. 16 | type ModelManager interface { 17 | 18 | // Identifiable returns an Identifiable with the given identity. 19 | Identifiable(Identity) Identifiable 20 | 21 | // SparseIdentifiable returns a SparseIdentifiable with the given identity. 22 | SparseIdentifiable(Identity) SparseIdentifiable 23 | 24 | // IdentifiableFromString returns an Identifiable from the given 25 | // string. The string can be an Identity name, category or alias. 26 | IdentifiableFromString(string) Identifiable 27 | 28 | // Identifiables returns an Identifiables with the given identity. 29 | Identifiables(Identity) Identifiables 30 | 31 | // SparseIdentifiables returns an Identifiables with the given identity. 32 | SparseIdentifiables(Identity) SparseIdentifiables 33 | 34 | // IdentifiablesFrom returns an Identifiables from the given 35 | // string. The string can be an Identity name, category or alias. 36 | IdentifiablesFromString(string) Identifiables 37 | 38 | // IdentityFromName returns the Identity from the given name. 39 | IdentityFromName(string) Identity 40 | 41 | // IdentityFromCategory returns the Identity from the given category. 42 | IdentityFromCategory(string) Identity 43 | 44 | // IdentityFromAlias returns the Identity from the given alias. 45 | IdentityFromAlias(string) Identity 46 | 47 | // IdentityFromAny returns the Identity from the given name, category or alias. 48 | IdentityFromAny(string) Identity 49 | 50 | // IndexesForIdentity returns the indexes of the given Identity. 51 | Indexes(Identity) [][]string 52 | 53 | // Relationships return the model's elemental.RelationshipsRegistry. 54 | Relationships() RelationshipsRegistry 55 | 56 | // AllIdentities return the list of all existing identities. 57 | AllIdentities() []Identity 58 | } 59 | -------------------------------------------------------------------------------- /identity_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "testing" 16 | 17 | . "github.com/smartystreets/goconvey/convey" 18 | ) 19 | 20 | func TestIdentity_AllIdentity(t *testing.T) { 21 | 22 | Convey("Given I retrieve the AllIdentity", t, func() { 23 | i := AllIdentity 24 | 25 | Convey("Then Name should *", func() { 26 | So(i.Name, ShouldEqual, "*") 27 | }) 28 | 29 | Convey("Then Category should *", func() { 30 | So(i.Category, ShouldEqual, "*") 31 | }) 32 | }) 33 | } 34 | 35 | func TestIdentity_MakeIdentity(t *testing.T) { 36 | 37 | Convey("Given I create a new identity", t, func() { 38 | i := MakeIdentity("n", "c") 39 | 40 | Convey("Then Name should n", func() { 41 | So(i.Name, ShouldEqual, "n") 42 | }) 43 | 44 | Convey("Then Category should c", func() { 45 | So(i.Category, ShouldEqual, "c") 46 | }) 47 | }) 48 | } 49 | 50 | func TestIdentity_String(t *testing.T) { 51 | 52 | Convey("Given I create a new identity", t, func() { 53 | i := MakeIdentity("n", "c") 54 | 55 | Convey("Then String should ", func() { 56 | So(i.String(), ShouldEqual, "") 57 | }) 58 | }) 59 | } 60 | 61 | func TestIdentity_IsEmpty(t *testing.T) { 62 | 63 | Convey("Given I create a new emtpty identity", t, func() { 64 | i := Identity{} 65 | 66 | Convey("Then IsEmpty should return true", func() { 67 | So(i.IsEmpty(), ShouldBeTrue) 68 | }) 69 | }) 70 | 71 | Convey("Given I create a new non emtpty identity", t, func() { 72 | i := MakeIdentity("a", "b") 73 | 74 | Convey("Then IsEmpty should return false", func() { 75 | So(i.IsEmpty(), ShouldBeFalse) 76 | }) 77 | }) 78 | } 79 | 80 | func TestIdentity_Identity_Copy(t *testing.T) { 81 | 82 | Convey("Given I create I have a Identifiables with 2 Identifiable", t, func() { 83 | 84 | l1 := NewList() 85 | l1.ID = "x" 86 | 87 | l2 := NewList() 88 | l2.ID = "y" 89 | 90 | lst1 := ListsList{l1, l2} 91 | 92 | Convey("When I create copy", func() { 93 | 94 | lst2 := lst1.Copy() 95 | 96 | Convey("Then the copy should be correct", func() { 97 | So(len(lst1.List()), ShouldEqual, len(lst2.List())) 98 | So(lst1.List()[0].Identifier(), ShouldEqual, lst2.List()[0].Identifier()) 99 | So(lst1.List()[1].Identifier(), ShouldEqual, lst2.List()[1].Identifier()) 100 | }) 101 | }) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.aporeto.io/elemental 2 | 3 | go 1.24 4 | 5 | require go.aporeto.io/regolithe v1.72.1-0.20250502225552-687106e402e5 6 | 7 | require ( 8 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de 9 | github.com/getkin/kin-openapi v0.113.0 10 | github.com/go-test/deep v1.0.8 11 | github.com/gofrs/uuid v4.4.0+incompatible 12 | github.com/golang/mock v1.6.0 13 | github.com/mitchellh/copystructure v1.2.0 14 | github.com/smartystreets/goconvey v1.8.1 15 | github.com/ugorji/go/codec v1.2.8 16 | go.uber.org/zap v1.27.0 17 | golang.org/x/sync v0.13.0 18 | golang.org/x/text v0.24.0 19 | golang.org/x/tools v0.32.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | ) 22 | 23 | require ( 24 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 25 | github.com/sagikazarmark/locafero v0.9.0 // indirect 26 | github.com/smarty/assertions v1.15.0 // indirect 27 | github.com/sourcegraph/conc v0.3.0 // indirect 28 | ) 29 | 30 | require ( 31 | github.com/Microsoft/go-winio v0.6.2 // indirect 32 | github.com/emirpasic/gods v1.18.1 // indirect 33 | github.com/fatih/structs v1.1.0 // indirect 34 | github.com/fsnotify/fsnotify v1.9.0 // indirect 35 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 36 | github.com/go-openapi/swag v0.22.3 // indirect 37 | github.com/gopherjs/gopherjs v1.17.2 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/invopop/yaml v0.2.0 // indirect 40 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/jtolds/gls v4.20.0+incompatible // indirect 43 | github.com/kevinburke/ssh_config v1.2.0 // indirect 44 | github.com/mailru/easyjson v0.7.7 // indirect 45 | github.com/mitchellh/go-homedir v1.1.0 // indirect 46 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 47 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 48 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 49 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 50 | github.com/perimeterx/marshmallow v1.1.4 // indirect 51 | github.com/sergi/go-diff v1.3.1 // indirect 52 | github.com/spf13/afero v1.14.0 // indirect 53 | github.com/spf13/cast v1.8.0 // indirect 54 | github.com/spf13/cobra v1.9.1 // indirect 55 | github.com/spf13/pflag v1.0.6 // indirect 56 | github.com/spf13/viper v1.20.1 // indirect 57 | github.com/src-d/gcfg v1.4.0 // indirect 58 | github.com/subosito/gotenv v1.6.0 // indirect 59 | github.com/xanzy/ssh-agent v0.3.3 // indirect 60 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 61 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 62 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 63 | go.mongodb.org/mongo-driver v1.16.0 64 | go.uber.org/multierr v1.11.0 // indirect 65 | golang.org/x/crypto v0.37.0 // indirect 66 | golang.org/x/mod v0.24.0 // indirect 67 | golang.org/x/net v0.39.0 // indirect 68 | golang.org/x/sys v0.32.0 // indirect 69 | gopkg.in/ini.v1 v1.67.0 // indirect 70 | gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect 71 | gopkg.in/src-d/go-git.v4 v4.13.1 // indirect 72 | gopkg.in/warnings.v0 v0.1.2 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /internal/attribute_specifiable.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: go.aporeto.io/elemental (interfaces: AttributeSpecifiable) 3 | 4 | // Package internal is a generated GoMock package. 5 | package internal 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | elemental "go.aporeto.io/elemental" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockAttributeSpecifiable is a mock of AttributeSpecifiable interface 14 | type MockAttributeSpecifiable struct { 15 | ctrl *gomock.Controller 16 | recorder *MockAttributeSpecifiableMockRecorder 17 | } 18 | 19 | // MockAttributeSpecifiableMockRecorder is the mock recorder for MockAttributeSpecifiable 20 | type MockAttributeSpecifiableMockRecorder struct { 21 | mock *MockAttributeSpecifiable 22 | } 23 | 24 | // NewMockAttributeSpecifiable creates a new mock instance 25 | func NewMockAttributeSpecifiable(ctrl *gomock.Controller) *MockAttributeSpecifiable { 26 | mock := &MockAttributeSpecifiable{ctrl: ctrl} 27 | mock.recorder = &MockAttributeSpecifiableMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockAttributeSpecifiable) EXPECT() *MockAttributeSpecifiableMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // AttributeSpecifications mocks base method 37 | func (m *MockAttributeSpecifiable) AttributeSpecifications() map[string]elemental.AttributeSpecification { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "AttributeSpecifications") 40 | ret0, _ := ret[0].(map[string]elemental.AttributeSpecification) 41 | return ret0 42 | } 43 | 44 | // AttributeSpecifications indicates an expected call of AttributeSpecifications 45 | func (mr *MockAttributeSpecifiableMockRecorder) AttributeSpecifications() *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AttributeSpecifications", reflect.TypeOf((*MockAttributeSpecifiable)(nil).AttributeSpecifications)) 48 | } 49 | 50 | // SpecificationForAttribute mocks base method 51 | func (m *MockAttributeSpecifiable) SpecificationForAttribute(arg0 string) elemental.AttributeSpecification { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "SpecificationForAttribute", arg0) 54 | ret0, _ := ret[0].(elemental.AttributeSpecification) 55 | return ret0 56 | } 57 | 58 | // SpecificationForAttribute indicates an expected call of SpecificationForAttribute 59 | func (mr *MockAttributeSpecifiableMockRecorder) SpecificationForAttribute(arg0 any) *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpecificationForAttribute", reflect.TypeOf((*MockAttributeSpecifiable)(nil).SpecificationForAttribute), arg0) 62 | } 63 | 64 | // ValueForAttribute mocks base method 65 | func (m *MockAttributeSpecifiable) ValueForAttribute(arg0 string) any { 66 | m.ctrl.T.Helper() 67 | ret := m.ctrl.Call(m, "ValueForAttribute", arg0) 68 | ret0, _ := ret[0].(any) 69 | return ret0 70 | } 71 | 72 | // ValueForAttribute indicates an expected call of ValueForAttribute 73 | func (mr *MockAttributeSpecifiableMockRecorder) ValueForAttribute(arg0 any) *gomock.Call { 74 | mr.mock.ctrl.T.Helper() 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValueForAttribute", reflect.TypeOf((*MockAttributeSpecifiable)(nil).ValueForAttribute), arg0) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/converter_errors_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "go.aporeto.io/regolithe/spec" 11 | ) 12 | 13 | func TestConverter_Do__error_bad_externalType_mapping(t *testing.T) { 14 | t.Parallel() 15 | 16 | specDir := t.TempDir() 17 | 18 | badTypeMapping := replaceTrailingTabsWithDoubleSpaceForYAML(` 19 | '[]byte': 20 | openapi3: 21 | type: malformed-json } 22 | `) 23 | 24 | rawSpec := replaceTrailingTabsWithDoubleSpaceForYAML(` 25 | model: 26 | rest_name: test 27 | resource_name: tests 28 | entity_name: Test 29 | package: None 30 | group: N/A 31 | description: dummy. 32 | attributes: 33 | v1: 34 | - name: someField 35 | description: useful description. 36 | type: external 37 | subtype: '[]byte' 38 | exposed: true 39 | `) 40 | 41 | for filename, content := range map[string]string{ 42 | "regolithe.ini": regolitheINI, 43 | "_type.mapping": badTypeMapping, 44 | "test.spec": rawSpec, 45 | } { 46 | filename = filepath.Join(specDir, filename) 47 | if err := os.WriteFile(filename, []byte(content), os.ModePerm); err != nil { 48 | t.Fatalf("error writing temporary file '%s': %v", filename, err) 49 | } 50 | } 51 | 52 | specif, err := spec.LoadSpecificationSet(specDir, nil, nil, "openapi3") 53 | if err != nil { 54 | t.Fatalf("error parsing spec set from test data: %v", err) 55 | } 56 | 57 | converter := newConverter(specif, Config{}) 58 | if err := converter.Do(nil); !errors.Is(err, errUnmarshalingExternalType) { 59 | t.Fatalf("unexpected error\nwant: %v\n got: %v", errUnmarshalingExternalType, err) 60 | } 61 | } 62 | 63 | func TestConverter_Do__error_writer(t *testing.T) { 64 | 65 | specDir := t.TempDir() 66 | 67 | rawSpec := replaceTrailingTabsWithDoubleSpaceForYAML(` 68 | model: 69 | rest_name: test 70 | resource_name: tests 71 | entity_name: Test 72 | package: None 73 | group: N/A 74 | description: dummy. 75 | `) 76 | 77 | for filename, content := range map[string]string{ 78 | "regolithe.ini": regolitheINI, 79 | "_type.mapping": typeMapping, 80 | "test.spec": rawSpec, 81 | } { 82 | filename = filepath.Join(specDir, filename) 83 | if err := os.WriteFile(filename, []byte(content), os.ModePerm); err != nil { 84 | t.Fatalf("error writing temporary file '%s': %v", filename, err) 85 | } 86 | } 87 | 88 | specif, err := spec.LoadSpecificationSet(specDir, nil, nil, "openapi3") 89 | if err != nil { 90 | t.Fatalf("error parsing spec set from test data: %v", err) 91 | } 92 | 93 | simulatedErr1 := errors.New("simulated error 1") 94 | fw := &fakeWriter{wrErr: simulatedErr1} 95 | writerFactory := func(string) (io.WriteCloser, error) { return fw, nil } 96 | converter := newConverter(specif, Config{}) 97 | if err := converter.Do(writerFactory); !errors.Is(err, simulatedErr1) { 98 | t.Fatalf("unexpected error\nwant: %v\n got: %v", simulatedErr1, err) 99 | } 100 | 101 | simulatedErr2 := errors.New("simulated error 2") 102 | writerFactory = func(string) (io.WriteCloser, error) { return nil, simulatedErr2 } 103 | converter = newConverter(specif, Config{}) 104 | if err := converter.Do(writerFactory); !errors.Is(err, simulatedErr2) { 105 | t.Fatalf("unexpected error\nwant: %v\n got: %v", simulatedErr2, err) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cmd/elegen/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "os" 17 | "path" 18 | 19 | "go.aporeto.io/elemental/cmd/elegen/versions" 20 | "go.aporeto.io/regolithe" 21 | "go.aporeto.io/regolithe/spec" 22 | "golang.org/x/sync/errgroup" 23 | 24 | "go.aporeto.io/elemental/cmd/internal/genopenapi3" 25 | ) 26 | 27 | const ( 28 | generatorName = "elegen" 29 | generatorDescription = "Generate a Go model based on elemental." 30 | generationName = "elemental" 31 | ) 32 | 33 | func main() { 34 | 35 | // will be initialized later 36 | var ( 37 | genType string 38 | publicMode bool 39 | splitOutput bool 40 | ) 41 | 42 | generator := func(sets []spec.SpecificationSet, out string) error { 43 | switch genType { 44 | case "openapi3": 45 | cfg := genopenapi3.Config{ 46 | Public: publicMode, 47 | SplitOutput: splitOutput, 48 | OutputDir: out, 49 | } 50 | return genopenapi3.GeneratorFunc(sets, cfg) 51 | case "", "elemental": 52 | return genElemental(sets, out, publicMode) 53 | default: 54 | return fmt.Errorf("unhandled generation type: '%s'", genType) 55 | } 56 | } 57 | 58 | version := fmt.Sprintf("%s - %s", versions.ProjectVersion, versions.ProjectSha) 59 | cmd := regolithe.NewCommand( 60 | generatorName, 61 | generatorDescription, 62 | version, 63 | attributeNameConverter, 64 | attributeTypeConverter, 65 | generationName, 66 | generator, 67 | ) 68 | 69 | cmd.PersistentFlags().BoolVar( 70 | &publicMode, 71 | "public", 72 | false, 73 | "If set to true, only exposed attributes and public objects will be generated", 74 | ) 75 | cmd.PersistentFlags().BoolVar( 76 | &splitOutput, 77 | "split-output", 78 | false, 79 | "If set to true, the openapi3 output will be split into multiple files", 80 | ) 81 | cmd.PersistentFlags().StringVarP( 82 | &genType, 83 | "gen-type", 84 | "g", 85 | "elemental", 86 | "The desired type of what needs to be generated. Possible choices are: [elemental openapi3]", 87 | ) 88 | 89 | if err := cmd.Execute(); err != nil { 90 | fmt.Fprint(os.Stderr, err) // nolint 91 | os.Exit(1) 92 | } 93 | } 94 | 95 | func genElemental(sets []spec.SpecificationSet, out string, publicMode bool) error { 96 | 97 | set := sets[0] 98 | outFolder := path.Join(out, "elemental") 99 | if err := os.MkdirAll(outFolder, 0750); err != nil && !os.IsExist(err) { 100 | return err 101 | } 102 | 103 | var g errgroup.Group 104 | 105 | g.Go(func() error { return writeIdentitiesRegistry(set, outFolder, publicMode) }) 106 | g.Go(func() error { return writeRelationshipsRegistry(set, outFolder, publicMode) }) 107 | 108 | for _, s := range set.Specifications() { 109 | func(restName string) { 110 | g.Go(func() error { return writeModel(set, restName, outFolder, publicMode) }) 111 | }(s.Model().RestName) 112 | } 113 | 114 | return g.Wait() 115 | } 116 | -------------------------------------------------------------------------------- /relationships.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | // A RelationshipInfo describe the various meta information of a relationship. 15 | type RelationshipInfo struct { 16 | Deprecated bool 17 | Parameters []ParameterDefinition 18 | RequiredParameters ParametersRequirement 19 | } 20 | 21 | // A RelationshipsRegistry maintains the relationship for Identities. 22 | type RelationshipsRegistry map[Identity]*Relationship 23 | 24 | // A Relationship describes the hierarchical relationship of the models. 25 | type Relationship struct { 26 | Type string 27 | 28 | Retrieve map[string]*RelationshipInfo 29 | RetrieveMany map[string]*RelationshipInfo 30 | Info map[string]*RelationshipInfo 31 | Create map[string]*RelationshipInfo 32 | Update map[string]*RelationshipInfo 33 | Delete map[string]*RelationshipInfo 34 | Patch map[string]*RelationshipInfo 35 | } 36 | 37 | // RelationshipInfoForOperation returns the relationship info for the given identity, parent identity and operation. 38 | func RelationshipInfoForOperation(registry RelationshipsRegistry, i Identity, pid Identity, op Operation) *RelationshipInfo { 39 | 40 | r, ok := registry[i] 41 | if !ok { 42 | return nil 43 | } 44 | 45 | switch op { 46 | case OperationCreate: 47 | return r.Create[pid.Name] 48 | case OperationDelete: 49 | return r.Delete[pid.Name] 50 | case OperationInfo: 51 | return r.Info[pid.Name] 52 | case OperationPatch: 53 | return r.Patch[pid.Name] 54 | case OperationRetrieve: 55 | return r.Retrieve[pid.Name] 56 | case OperationRetrieveMany: 57 | return r.RetrieveMany[pid.Name] 58 | case OperationUpdate: 59 | return r.Update[pid.Name] 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // IsOperationAllowed returns true if given operatation on the given identity with the given parent is allowed. 66 | func IsOperationAllowed(registry RelationshipsRegistry, i Identity, pid Identity, op Operation) bool { 67 | 68 | r, ok := registry[i] 69 | if !ok { 70 | return false 71 | } 72 | 73 | switch op { 74 | case OperationCreate: 75 | _, ok = r.Create[pid.Name] 76 | case OperationDelete: 77 | _, ok = r.Delete[pid.Name] 78 | case OperationInfo: 79 | _, ok = r.Info[pid.Name] 80 | case OperationPatch: 81 | _, ok = r.Patch[pid.Name] 82 | case OperationRetrieve: 83 | _, ok = r.Retrieve[pid.Name] 84 | case OperationRetrieveMany: 85 | _, ok = r.RetrieveMany[pid.Name] 86 | case OperationUpdate: 87 | _, ok = r.Update[pid.Name] 88 | default: 89 | return false 90 | } 91 | 92 | return ok 93 | } 94 | 95 | // ParametersForOperation returns the parameters defined for the retrieve operation on the given identity. 96 | func ParametersForOperation(registry RelationshipsRegistry, i Identity, pid Identity, op Operation) []ParameterDefinition { 97 | 98 | rel := RelationshipInfoForOperation(registry, i, pid, op) 99 | if rel == nil { 100 | return nil 101 | } 102 | 103 | return rel.Parameters 104 | } 105 | -------------------------------------------------------------------------------- /namespace.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Namespacer is the interface that any namespace extraction/injection 10 | // implementation should support. 11 | type Namespacer interface { 12 | Extract(r *http.Request) (string, error) 13 | Inject(r *http.Request, namespace string) error 14 | } 15 | 16 | var ( 17 | namespacer = Namespacer(&defaultNamespacer{}) 18 | ) 19 | 20 | type defaultNamespacer struct{} 21 | 22 | // defaultExtractor will retrieve the namespace value from the header X-Namespace. 23 | func (d *defaultNamespacer) Extract(r *http.Request) (string, error) { 24 | return r.Header.Get("X-Namespace"), nil 25 | } 26 | 27 | // defaultInjector will set the namespace as an HTTP header. 28 | func (d *defaultNamespacer) Inject(r *http.Request, namespace string) error { 29 | if r.Header == nil { 30 | r.Header = http.Header{} 31 | } 32 | r.Header.Set("X-Namespace", namespace) 33 | return nil 34 | } 35 | 36 | // SetNamespacer will configure the package. It must be only called once 37 | // and it is global for the package. 38 | func SetNamespacer(custom Namespacer) { 39 | namespacer = custom 40 | } 41 | 42 | // GetNamespacer retrieves the configured namespacer. 43 | func GetNamespacer() Namespacer { 44 | return namespacer 45 | } 46 | 47 | // ParentNamespaceFromString returns the parent namespace of a namespace 48 | // It returns empty it the string is invalid 49 | func ParentNamespaceFromString(namespace string) (string, error) { 50 | 51 | if namespace == "" { 52 | return "", fmt.Errorf("invalid empty namespace name") 53 | } 54 | 55 | if namespace == "/" { 56 | return "", nil 57 | } 58 | 59 | index := strings.LastIndex(namespace, "/") 60 | 61 | switch index { 62 | case -1: 63 | return "", fmt.Errorf("invalid namespace name") 64 | case 0: 65 | return namespace[:index+1], nil 66 | default: 67 | return namespace[:index], nil 68 | } 69 | } 70 | 71 | // IsNamespaceRelatedToNamespace returns true if the given namespace is related to the given parent 72 | func IsNamespaceRelatedToNamespace(ns string, parent string) bool { 73 | return IsNamespaceParentOfNamespace(ns, parent) || 74 | IsNamespaceChildrenOfNamespace(ns, parent) || 75 | (ns == parent && ns != "" && parent != "") 76 | } 77 | 78 | // IsNamespaceParentOfNamespace returns true if the given namespace is a parent of the given parent 79 | func IsNamespaceParentOfNamespace(ns string, child string) bool { 80 | 81 | if ns == "" || child == "" { 82 | return false 83 | } 84 | 85 | if ns == child { 86 | return false 87 | } 88 | 89 | if ns[len(ns)-1] != '/' { 90 | ns = ns + "/" 91 | } 92 | 93 | return strings.HasPrefix(child, ns) 94 | } 95 | 96 | // IsNamespaceChildrenOfNamespace returns true of the given ns is a children of the given parent. 97 | func IsNamespaceChildrenOfNamespace(ns string, parent string) bool { 98 | 99 | if parent == "" || ns == "" { 100 | return false 101 | } 102 | 103 | if ns == parent { 104 | return false 105 | } 106 | 107 | if parent[len(parent)-1] != '/' { 108 | parent = parent + "/" 109 | } 110 | 111 | return strings.HasPrefix(ns, parent) 112 | } 113 | 114 | // NamespaceAncestorsNames returns the list of fully qualified namespaces 115 | // in the hierarchy of a given namespace. It returns an empty 116 | // array for the root namespace 117 | func NamespaceAncestorsNames(namespace string) []string { 118 | 119 | if namespace == "/" || namespace == "" { 120 | return []string{} 121 | } 122 | 123 | parts := strings.Split(namespace, "/") 124 | sep := "/" 125 | namespaces := []string{} 126 | 127 | for i := len(parts) - 1; i >= 2; i-- { 128 | namespaces = append(namespaces, sep+strings.Join(parts[1:i], sep)) 129 | } 130 | 131 | namespaces = append(namespaces, sep) 132 | 133 | return namespaces 134 | } 135 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/gen_model.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | "go.aporeto.io/regolithe/spec" 11 | ) 12 | 13 | var ( 14 | errUnmarshalingExternalType = errors.New("unmarshaling openapi3 external type mapping") 15 | ) 16 | 17 | func (c *converter) convertModel(s spec.Specification) (*openapi3.SchemaRef, error) { 18 | 19 | schema := openapi3.NewObjectSchema() 20 | schema.Description = s.Model().Description 21 | schema.Properties = make(map[string]*openapi3.SchemaRef) 22 | 23 | for _, specAttr := range s.Attributes("") { // TODO: figure out versions 24 | 25 | if !specAttr.Exposed { 26 | continue 27 | } 28 | 29 | attr, err := c.convertAttribute(specAttr) 30 | if err != nil { 31 | return nil, fmt.Errorf("attribute '%s': %w", specAttr.Name, err) 32 | } 33 | schema.Properties[specAttr.Name] = attr 34 | 35 | if specAttr.Required { 36 | schema.Required = append(schema.Required, specAttr.Name) 37 | } 38 | } 39 | 40 | sort.Strings(schema.Required) 41 | 42 | return openapi3.NewSchemaRef("", schema), nil 43 | } 44 | 45 | func (c *converter) convertAttribute(attr *spec.Attribute) (schemaRef *openapi3.SchemaRef, err error) { 46 | 47 | defer func() { 48 | if schemaRef == nil || schemaRef.Value == nil { 49 | return 50 | } 51 | s := schemaRef.Value 52 | s.Description = attr.Description 53 | s.Default = attr.DefaultValue 54 | s.Example = attr.ExampleValue 55 | s.Deprecated = attr.Deprecated 56 | s.ReadOnly = attr.ReadOnly 57 | }() 58 | 59 | switch attr.Type { 60 | 61 | case spec.AttributeTypeString: 62 | return openapi3.NewStringSchema().NewRef(), nil 63 | 64 | case spec.AttributeTypeInt: 65 | return openapi3.NewIntegerSchema().NewRef(), nil 66 | 67 | case spec.AttributeTypeFloat: 68 | return openapi3.NewFloat64Schema().NewRef(), nil 69 | 70 | case spec.AttributeTypeBool: 71 | return openapi3.NewBoolSchema().NewRef(), nil 72 | 73 | case spec.AttributeTypeTime: 74 | return openapi3.NewDateTimeSchema().NewRef(), nil 75 | 76 | case spec.AttributeTypeEnum: 77 | enumVals := make([]any, len(attr.AllowedChoices)) 78 | for i, val := range attr.AllowedChoices { 79 | enumVals[i] = val 80 | } 81 | return openapi3.NewSchema().WithEnum(enumVals...).NewRef(), nil 82 | 83 | case spec.AttributeTypeObject: 84 | return openapi3.NewObjectSchema().NewRef(), nil 85 | 86 | case spec.AttributeTypeList: 87 | attrSchema := openapi3.NewArraySchema() 88 | attr, err := c.convertAttribute(&spec.Attribute{Type: spec.AttributeType(attr.SubType)}) 89 | attrSchema.Items = attr 90 | return attrSchema.NewRef(), err // do not wrap error to avoid recursive wrapping 91 | 92 | case spec.AttributeTypeRef: 93 | if c.splitOutput { 94 | return openapi3.NewSchemaRef("./"+attr.SubType+"#/components/schemas/"+attr.SubType, nil), nil 95 | } 96 | return openapi3.NewSchemaRef("#/components/schemas/"+attr.SubType, nil), nil 97 | 98 | case spec.AttributeTypeRefList: 99 | attrSchema := openapi3.NewArraySchema() 100 | attr, err := c.convertAttribute(&spec.Attribute{Type: spec.AttributeTypeRef, SubType: attr.SubType}) 101 | attrSchema.Items = attr 102 | return attrSchema.NewRef(), err // do not wrap error to avoid recursive wrapping 103 | 104 | case spec.AttributeTypeRefMap: 105 | attrSchema := openapi3.NewObjectSchema() 106 | attr, err := c.convertAttribute(&spec.Attribute{Type: spec.AttributeTypeRef, SubType: attr.SubType}) 107 | attrSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: attr} 108 | return attrSchema.NewRef(), err // do not wrap error to avoid recursive wrapping 109 | 110 | case spec.AttributeTypeExt: 111 | mapping, err := c.inSpecSet.TypeMapping().Mapping("openapi3", attr.SubType) 112 | if err != nil { 113 | return nil, fmt.Errorf("retrieving 'openapi3' type mapping for external attribute subtype '%s': %w", attr.SubType, err) 114 | } 115 | 116 | attrSchema := new(openapi3.Schema) 117 | if err := json.Unmarshal([]byte(mapping.Type), attrSchema); err != nil { 118 | return nil, fmt.Errorf("%w: '%s': %s", errUnmarshalingExternalType, attr.SubType, err) 119 | } 120 | 121 | return attrSchema.NewRef(), nil 122 | } 123 | 124 | return nil, fmt.Errorf("unhandled attribute type: '%s'", attr.Type) 125 | } 126 | -------------------------------------------------------------------------------- /filterscanner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "bytes" 16 | "strings" 17 | "unicode/utf8" 18 | ) 19 | 20 | // scanner scans a given input 21 | type scanner struct { 22 | buf bytes.Buffer 23 | isWhitespace checkRuneFunc 24 | isLetter checkRuneFunc 25 | isDigit checkRuneFunc 26 | } 27 | 28 | // newScanner returns an instance of a Scanner. 29 | func newScanner( 30 | input string, 31 | ) *scanner { 32 | var buf bytes.Buffer 33 | _, _ = buf.WriteString(input) 34 | 35 | return &scanner{ 36 | buf: buf, 37 | isWhitespace: isWhitespace, 38 | isLetter: isLetter, 39 | isDigit: isDigit, 40 | } 41 | } 42 | 43 | // read returns the next rune or eof 44 | func (s *scanner) read() rune { 45 | 46 | ch, _, err := s.buf.ReadRune() 47 | if err != nil { 48 | return runeEOF 49 | } 50 | return ch 51 | } 52 | 53 | // peekNextRune returns the next rune but does not read it. 54 | func (s *scanner) peekNextRune() rune { 55 | 56 | if s.buf.Len() == 0 { 57 | return runeEOF 58 | } 59 | 60 | ch, _ := utf8.DecodeRune(s.buf.Bytes()[0:]) 61 | return ch 62 | } 63 | 64 | // unread a previously read rune 65 | func (s *scanner) unread() { 66 | _ = s.buf.UnreadRune() 67 | } 68 | 69 | // scan returns the next token and literal value. 70 | func (s *scanner) scan() (parserToken, string) { 71 | 72 | ch := s.read() 73 | 74 | if s.isWhitespace(ch) { 75 | s.unread() 76 | return s.scanWhitespace() 77 | } 78 | 79 | if isOperatorStart(ch) { 80 | // Chack if the next run can create an operator 81 | nextCh := s.peekNextRune() 82 | if nextCh != runeEOF { 83 | if token, literal, ok := isOperator(ch, nextCh); ok { 84 | s.read() // read only if it has matched. 85 | return token, literal 86 | } 87 | } 88 | 89 | // Check if the current rune is an operator 90 | if token, literal, ok := isOperator(ch); ok { 91 | return token, literal 92 | } 93 | } 94 | 95 | if s.isLetter(ch) || s.isDigit(ch) { 96 | s.unread() 97 | return s.scanWord() 98 | } 99 | 100 | token, ok := runeToToken[ch] 101 | if !ok { 102 | return parserTokenILLEGAL, string(ch) 103 | } 104 | 105 | return token, string(ch) 106 | } 107 | 108 | // scanWhitespace consumes the current rune and all contiguous whitespace. 109 | func (s *scanner) scanWhitespace() (parserToken, string) { 110 | 111 | var buf bytes.Buffer 112 | _, _ = buf.WriteRune(s.read()) 113 | 114 | for { 115 | if ch := s.read(); ch == runeEOF { 116 | break 117 | } else if !s.isWhitespace(ch) { 118 | s.unread() 119 | break 120 | } else { 121 | _, _ = buf.WriteRune(ch) 122 | } 123 | } 124 | 125 | return parserTokenWHITESPACE, buf.String() 126 | } 127 | 128 | // scanWord consumes the current rune and all contiguous letters / digits. 129 | func (s *scanner) scanWord() (parserToken, string) { 130 | 131 | var buf bytes.Buffer 132 | _, _ = buf.WriteRune(s.read()) 133 | 134 | for { 135 | if ch := s.read(); ch == runeEOF { 136 | break 137 | } else if !s.isLetter(ch) && !s.isDigit(ch) { 138 | s.unread() 139 | break 140 | } else { 141 | if ch == '\\' { 142 | // Move forward 143 | ch = s.read() 144 | } 145 | 146 | if isOperatorStart(ch) { 147 | // Check if the next rune can create an operator 148 | nextCh := s.peekNextRune() 149 | if nextCh != runeEOF { 150 | if _, _, ok := isOperator(ch, nextCh); ok { 151 | s.unread() // unread ch rune 152 | break 153 | } 154 | } 155 | 156 | // Check if ch is an operator by itself 157 | if _, _, ok := isOperator(ch); ok { 158 | s.unread() 159 | break 160 | } 161 | } 162 | 163 | _, _ = buf.WriteRune(ch) 164 | } 165 | } 166 | 167 | return stringToToken(buf.String()) 168 | } 169 | 170 | func stringToToken(output string) (parserToken, string) { 171 | 172 | upper := strings.ToUpper(output) 173 | 174 | if token, ok := wordToToken[upper]; ok { 175 | return token, output 176 | } 177 | 178 | if token, ok := operatorsToToken[upper]; ok { 179 | return token, output 180 | } 181 | 182 | return parserTokenWORD, output 183 | } 184 | -------------------------------------------------------------------------------- /test/model/unmarshalable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package testmodel 13 | 14 | import ( 15 | "fmt" 16 | 17 | "go.aporeto.io/elemental" 18 | ) 19 | 20 | // UnmarshalableListIdentity represents the Identity of the object. 21 | var UnmarshalableListIdentity = elemental.Identity{Name: "list", Category: "lists"} 22 | 23 | // UnmarshalableListsList represents a list of UnmarshalableLists 24 | type UnmarshalableListsList []*UnmarshalableList 25 | 26 | // Identity returns the identity of the objects in the list. 27 | func (o UnmarshalableListsList) Identity() elemental.Identity { 28 | 29 | return UnmarshalableListIdentity 30 | } 31 | 32 | // Copy returns a pointer to a copy the UnmarshalableListsList. 33 | func (o UnmarshalableListsList) Copy() elemental.Identifiables { 34 | 35 | out := append(UnmarshalableListsList{}, o...) 36 | return &out 37 | } 38 | 39 | // Append appends the objects to the a new copy of the UnmarshalableListsList. 40 | func (o UnmarshalableListsList) Append(objects ...elemental.Identifiable) elemental.Identifiables { 41 | 42 | out := append(UnmarshalableListsList{}, o...) 43 | for _, obj := range objects { 44 | out = append(out, obj.(*UnmarshalableList)) 45 | } 46 | 47 | return out 48 | } 49 | 50 | // List converts the object to an elemental.IdentifiablesList. 51 | func (o UnmarshalableListsList) List() elemental.IdentifiablesList { 52 | 53 | out := elemental.IdentifiablesList{} 54 | for _, item := range o { 55 | out = append(out, item) 56 | } 57 | 58 | return out 59 | } 60 | 61 | // DefaultOrder returns the default ordering fields of the content. 62 | func (o UnmarshalableListsList) DefaultOrder() []string { 63 | 64 | return []string{ 65 | "flagDefaultOrderingKey", 66 | } 67 | } 68 | 69 | // Version returns the version of the content. 70 | func (o UnmarshalableListsList) Version() int { 71 | 72 | return 1 73 | } 74 | 75 | // An UnmarshalableList is a List that cannot be marshalled or unmarshalled. 76 | type UnmarshalableList struct { 77 | List 78 | } 79 | 80 | // NewUnmarshalableList returns a new UnmarshalableList. 81 | func NewUnmarshalableList() *UnmarshalableList { 82 | return &UnmarshalableList{List: List{}} 83 | } 84 | 85 | // Identity returns the identity. 86 | func (o *UnmarshalableList) Identity() elemental.Identity { return UnmarshalableListIdentity } 87 | 88 | // UnmarshalJSON makes the UnmarshalableList not unmarshalable. 89 | func (o *UnmarshalableList) UnmarshalJSON([]byte) error { 90 | return fmt.Errorf("error unmarshalling") 91 | } 92 | 93 | // MarshalJSON makes the UnmarshalableList not marshalable. 94 | func (o *UnmarshalableList) MarshalJSON() ([]byte, error) { 95 | return nil, fmt.Errorf("error marshalling") 96 | } 97 | 98 | // UnmarshalMsgpack makes the UnmarshalableList not unmarshalable. 99 | func (o *UnmarshalableList) UnmarshalMsgpack([]byte) error { 100 | return fmt.Errorf("error unmarshalling") 101 | } 102 | 103 | // MarshalMsgpack makes the UnmarshalableList not marshalable. 104 | func (o *UnmarshalableList) MarshalMsgpack() ([]byte, error) { 105 | return nil, fmt.Errorf("error marshalling") 106 | } 107 | 108 | // Validate validates the data 109 | func (o *UnmarshalableList) Validate() elemental.Errors { return nil } 110 | 111 | // An UnmarshalableError is a List that cannot be marshalled or unmarshalled. 112 | type UnmarshalableError struct { 113 | elemental.Error 114 | } 115 | 116 | // UnmarshalJSON makes the UnmarshalableError not unmarshalable. 117 | func (o *UnmarshalableError) UnmarshalJSON([]byte) error { 118 | return fmt.Errorf("error unmarshalling") 119 | } 120 | 121 | // MarshalJSON makes the UnmarshalableError not marshalable. 122 | func (o *UnmarshalableError) MarshalJSON() ([]byte, error) { 123 | return nil, fmt.Errorf("error marshalling") 124 | } 125 | 126 | // UnmarshalMsgpack makes the UnmarshalableError not unmarshalable. 127 | func (o *UnmarshalableError) UnmarshalMsgpack([]byte) error { 128 | return fmt.Errorf("error unmarshalling") 129 | } 130 | 131 | // MarshalMsgpack makes the UnmarshalableError not marshalable. 132 | func (o *UnmarshalableError) MarshalMsgpack() ([]byte, error) { 133 | return nil, fmt.Errorf("error marshalling") 134 | } 135 | -------------------------------------------------------------------------------- /test/model/identities_registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by elegen. DO NOT EDIT. 2 | // Source: go.aporeto.io/elemental (templates/identities_registry.gotpl) 3 | 4 | package testmodel 5 | 6 | import "go.aporeto.io/elemental" 7 | 8 | var ( 9 | identityNamesMap = map[string]elemental.Identity{ 10 | "list": ListIdentity, 11 | "root": RootIdentity, 12 | "task": TaskIdentity, 13 | "user": UserIdentity, 14 | } 15 | 16 | identitycategoriesMap = map[string]elemental.Identity{ 17 | "lists": ListIdentity, 18 | "root": RootIdentity, 19 | "tasks": TaskIdentity, 20 | "users": UserIdentity, 21 | } 22 | 23 | aliasesMap = map[string]elemental.Identity{ 24 | "lst": ListIdentity, 25 | "tsk": TaskIdentity, 26 | "usr": UserIdentity, 27 | } 28 | 29 | indexesMap = map[string][][]string{ 30 | "list": nil, 31 | "root": nil, 32 | "task": nil, 33 | "user": nil, 34 | } 35 | ) 36 | 37 | // ModelVersion returns the current version of the model. 38 | func ModelVersion() float64 { return 1 } 39 | 40 | type modelManager struct{} 41 | 42 | func (f modelManager) IdentityFromName(name string) elemental.Identity { 43 | 44 | return identityNamesMap[name] 45 | } 46 | 47 | func (f modelManager) IdentityFromCategory(category string) elemental.Identity { 48 | 49 | return identitycategoriesMap[category] 50 | } 51 | 52 | func (f modelManager) IdentityFromAlias(alias string) elemental.Identity { 53 | 54 | return aliasesMap[alias] 55 | } 56 | 57 | func (f modelManager) IdentityFromAny(any string) (i elemental.Identity) { 58 | 59 | if i = f.IdentityFromName(any); !i.IsEmpty() { 60 | return i 61 | } 62 | 63 | if i = f.IdentityFromCategory(any); !i.IsEmpty() { 64 | return i 65 | } 66 | 67 | return f.IdentityFromAlias(any) 68 | } 69 | 70 | func (f modelManager) Identifiable(identity elemental.Identity) elemental.Identifiable { 71 | 72 | switch identity { 73 | 74 | case ListIdentity: 75 | return NewList() 76 | case RootIdentity: 77 | return NewRoot() 78 | case TaskIdentity: 79 | return NewTask() 80 | case UserIdentity: 81 | return NewUser() 82 | default: 83 | return nil 84 | } 85 | } 86 | 87 | func (f modelManager) SparseIdentifiable(identity elemental.Identity) elemental.SparseIdentifiable { 88 | 89 | switch identity { 90 | 91 | case ListIdentity: 92 | return NewSparseList() 93 | case TaskIdentity: 94 | return NewSparseTask() 95 | case UserIdentity: 96 | return NewSparseUser() 97 | default: 98 | return nil 99 | } 100 | } 101 | 102 | func (f modelManager) Indexes(identity elemental.Identity) [][]string { 103 | 104 | return indexesMap[identity.Name] 105 | } 106 | 107 | func (f modelManager) IdentifiableFromString(any string) elemental.Identifiable { 108 | 109 | return f.Identifiable(f.IdentityFromAny(any)) 110 | } 111 | 112 | func (f modelManager) Identifiables(identity elemental.Identity) elemental.Identifiables { 113 | 114 | switch identity { 115 | 116 | case ListIdentity: 117 | return &ListsList{} 118 | case TaskIdentity: 119 | return &TasksList{} 120 | case UserIdentity: 121 | return &UsersList{} 122 | default: 123 | return nil 124 | } 125 | } 126 | 127 | func (f modelManager) SparseIdentifiables(identity elemental.Identity) elemental.SparseIdentifiables { 128 | 129 | switch identity { 130 | 131 | case ListIdentity: 132 | return &SparseListsList{} 133 | case TaskIdentity: 134 | return &SparseTasksList{} 135 | case UserIdentity: 136 | return &SparseUsersList{} 137 | default: 138 | return nil 139 | } 140 | } 141 | 142 | func (f modelManager) IdentifiablesFromString(any string) elemental.Identifiables { 143 | 144 | return f.Identifiables(f.IdentityFromAny(any)) 145 | } 146 | 147 | func (f modelManager) Relationships() elemental.RelationshipsRegistry { 148 | 149 | return relationshipsRegistry 150 | } 151 | 152 | func (f modelManager) AllIdentities() []elemental.Identity { 153 | return AllIdentities() 154 | } 155 | 156 | var manager = modelManager{} 157 | 158 | // Manager returns the model elemental.ModelManager. 159 | func Manager() elemental.ModelManager { return manager } 160 | 161 | // AllIdentities returns all existing identities. 162 | func AllIdentities() []elemental.Identity { 163 | 164 | return []elemental.Identity{ 165 | ListIdentity, 166 | RootIdentity, 167 | TaskIdentity, 168 | UserIdentity, 169 | } 170 | } 171 | 172 | // AliasesForIdentity returns all the aliases for the given identity. 173 | func AliasesForIdentity(identity elemental.Identity) []string { 174 | 175 | switch identity { 176 | case ListIdentity: 177 | return []string{ 178 | "lst", 179 | } 180 | case RootIdentity: 181 | return []string{} 182 | case TaskIdentity: 183 | return []string{ 184 | "tsk", 185 | } 186 | case UserIdentity: 187 | return []string{ 188 | "usr", 189 | } 190 | } 191 | 192 | return nil 193 | } 194 | -------------------------------------------------------------------------------- /test/model/root.go: -------------------------------------------------------------------------------- 1 | // Code generated by elegen. DO NOT EDIT. 2 | // Source: go.aporeto.io/elemental (templates/model.gotpl) 3 | 4 | package testmodel 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/mitchellh/copystructure" 10 | "go.aporeto.io/elemental" 11 | "go.mongodb.org/mongo-driver/bson" 12 | ) 13 | 14 | // RootIdentity represents the Identity of the object. 15 | var RootIdentity = elemental.Identity{ 16 | Name: "root", 17 | Category: "root", 18 | Package: "todo-list", 19 | Private: false, 20 | } 21 | 22 | // Root represents the model of a root 23 | type Root struct { 24 | ModelVersion int `json:"-" msgpack:"-" bson:"_modelversion"` 25 | } 26 | 27 | // NewRoot returns a new *Root 28 | func NewRoot() *Root { 29 | 30 | return &Root{ 31 | ModelVersion: 1, 32 | } 33 | } 34 | 35 | // Identity returns the Identity of the object. 36 | func (o *Root) Identity() elemental.Identity { 37 | 38 | return RootIdentity 39 | } 40 | 41 | // Identifier returns the value of the object's unique identifier. 42 | func (o *Root) Identifier() string { 43 | 44 | return "" 45 | } 46 | 47 | // SetIdentifier sets the value of the object's unique identifier. 48 | func (o *Root) SetIdentifier(id string) { 49 | 50 | } 51 | 52 | // MarshalBSON implements the bson marshaling interface. 53 | // This is used to transparently convert ID to MongoDBID as ObectID. 54 | func (o *Root) MarshalBSON() ([]byte, error) { 55 | 56 | if o == nil { 57 | return nil, nil 58 | } 59 | 60 | s := mongoAttributesRoot{} 61 | 62 | return bson.Marshal(s) 63 | } 64 | 65 | // UnmarshalBSON implements the bson unmarshaling interface. 66 | // This is used to transparently convert MongoDBID to ID. 67 | func (o *Root) UnmarshalBSON(raw []byte) error { 68 | 69 | if o == nil { 70 | return nil 71 | } 72 | 73 | s := &mongoAttributesRoot{} 74 | if err := bson.Unmarshal(raw, s); err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // Version returns the hardcoded version of the model. 82 | func (o *Root) Version() int { 83 | 84 | return 1 85 | } 86 | 87 | // BleveType implements the bleve.Classifier Interface. 88 | func (o *Root) BleveType() string { 89 | 90 | return "root" 91 | } 92 | 93 | // DefaultOrder returns the list of default ordering fields. 94 | func (o *Root) DefaultOrder() []string { 95 | 96 | return []string{} 97 | } 98 | 99 | // Doc returns the documentation for the object 100 | func (o *Root) Doc() string { 101 | 102 | return `Root object of the API.` 103 | } 104 | 105 | func (o *Root) String() string { 106 | 107 | return fmt.Sprintf("<%s:%s>", o.Identity().Name, o.Identifier()) 108 | } 109 | 110 | // DeepCopy returns a deep copy if the Root. 111 | func (o *Root) DeepCopy() *Root { 112 | 113 | if o == nil { 114 | return nil 115 | } 116 | 117 | out := &Root{} 118 | o.DeepCopyInto(out) 119 | 120 | return out 121 | } 122 | 123 | // DeepCopyInto copies the receiver into the given *Root. 124 | func (o *Root) DeepCopyInto(out *Root) { 125 | 126 | target, err := copystructure.Copy(o) 127 | if err != nil { 128 | panic(fmt.Sprintf("Unable to deepcopy Root: %s", err)) 129 | } 130 | 131 | *out = *target.(*Root) 132 | } 133 | 134 | // Validate valides the current information stored into the structure. 135 | func (o *Root) Validate() error { 136 | 137 | errors := elemental.Errors{} 138 | requiredErrors := elemental.Errors{} 139 | 140 | if len(requiredErrors) > 0 { 141 | return requiredErrors 142 | } 143 | 144 | if len(errors) > 0 { 145 | return errors 146 | } 147 | 148 | return nil 149 | } 150 | 151 | // SpecificationForAttribute returns the AttributeSpecification for the given attribute name key. 152 | func (*Root) SpecificationForAttribute(name string) elemental.AttributeSpecification { 153 | 154 | if v, ok := RootAttributesMap[name]; ok { 155 | return v 156 | } 157 | 158 | // We could not find it, so let's check on the lower case indexed spec map 159 | return RootLowerCaseAttributesMap[name] 160 | } 161 | 162 | // AttributeSpecifications returns the full attribute specifications map. 163 | func (*Root) AttributeSpecifications() map[string]elemental.AttributeSpecification { 164 | 165 | return RootAttributesMap 166 | } 167 | 168 | // ValueForAttribute returns the value for the given attribute. 169 | // This is a very advanced function that you should not need but in some 170 | // very specific use cases. 171 | func (o *Root) ValueForAttribute(name string) any { 172 | 173 | switch name { 174 | } 175 | 176 | return nil 177 | } 178 | 179 | // RootAttributesMap represents the map of attribute for Root. 180 | var RootAttributesMap = map[string]elemental.AttributeSpecification{} 181 | 182 | // RootLowerCaseAttributesMap represents the map of attribute for Root. 183 | var RootLowerCaseAttributesMap = map[string]elemental.AttributeSpecification{} 184 | 185 | type mongoAttributesRoot struct { 186 | } 187 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "reflect" 16 | "time" 17 | ) 18 | 19 | // RemoveZeroValues reset all pointer fields that are pointing to a zero value to nil 20 | func RemoveZeroValues(obj any) { 21 | 22 | vo := reflect.ValueOf(obj) 23 | vv := reflect.Indirect(vo) 24 | 25 | for _, field := range extractFieldNames(obj) { 26 | v := vv.FieldByName(field) 27 | 28 | if v.Kind() != reflect.Ptr { 29 | continue 30 | } 31 | 32 | if v.IsNil() { 33 | continue 34 | } 35 | 36 | uv := reflect.Indirect(v) 37 | 38 | switch uv.Kind() { 39 | case reflect.Map, reflect.Slice, reflect.Array: 40 | if uv.Len() == 0 { 41 | v.Set(reflect.Zero(v.Type())) 42 | break 43 | } 44 | 45 | fallthrough 46 | 47 | default: 48 | if uv.IsZero() { 49 | v.Set(reflect.Zero(v.Type())) 50 | } 51 | } 52 | } 53 | } 54 | 55 | // extractFieldNames returns all the field Name of the given 56 | // object using reflection. 57 | func extractFieldNames(obj any) []string { 58 | 59 | val := reflect.Indirect(reflect.ValueOf(obj)) 60 | c := val.NumField() 61 | fields := make([]string, c) 62 | 63 | for i := 0; i < c; i++ { 64 | fields[i] = val.Type().Field(i).Name 65 | } 66 | 67 | return fields 68 | } 69 | 70 | var reflectedTimeType = reflect.ValueOf(time.Time{}).Type() 71 | 72 | // areFieldValuesEqual checks if the value of the given field name are 73 | // equal in both given objects using reflection. 74 | func areFieldValuesEqual(field string, o1, o2 any) bool { 75 | 76 | field1 := reflect.Indirect(reflect.ValueOf(o1)).FieldByName(field) 77 | field2 := reflect.Indirect(reflect.ValueOf(o2)).FieldByName(field) 78 | 79 | if isFieldValueZero(field, o1) && isFieldValueZero(field, o2) { 80 | return true 81 | } 82 | 83 | // This is to handle time structure whatever their timezone 84 | if field1.Type() == reflectedTimeType { 85 | return field1.Interface().(time.Time).Unix() == field2.Interface().(time.Time).Unix() 86 | } 87 | 88 | if field1.Kind() == reflect.Slice || field1.Kind() == reflect.Array { 89 | 90 | if field1.Len() != field2.Len() { 91 | return false 92 | } 93 | 94 | // Same stuff we need to check all time element. 95 | if field1.Type().Elem() == reflectedTimeType { 96 | for i := 0; i < field1.Len(); i++ { 97 | if field1.Index(i).Interface().(time.Time).Unix() != field2.Index(i).Interface().(time.Time).Unix() { 98 | return false 99 | } 100 | } 101 | return true 102 | } 103 | 104 | return reflect.DeepEqual(field1.Interface(), field2.Interface()) 105 | } 106 | 107 | if field1.Kind() == reflect.Map { 108 | 109 | if field1.Len() != field2.Len() { 110 | return false 111 | } 112 | 113 | return reflect.DeepEqual(field1.Interface(), field2.Interface()) 114 | } 115 | 116 | return field1.Interface() == field2.Interface() 117 | } 118 | 119 | // isFieldValueZero check if the value of the given field is set to its zero value. 120 | func isFieldValueZero(field string, o any) bool { 121 | 122 | return IsZero(reflect.Indirect(reflect.ValueOf(o)).FieldByName(field).Interface()) 123 | } 124 | 125 | // IsZero returns true if the given value is set to its Zero value. 126 | func IsZero(o any) bool { 127 | 128 | if o == nil { 129 | return true 130 | } 131 | 132 | v := reflect.ValueOf(o) 133 | 134 | if v.IsZero() { 135 | return true 136 | } 137 | 138 | v = reflect.Indirect(v) 139 | 140 | switch v.Kind() { 141 | case reflect.Slice, reflect.Map: 142 | return v.IsNil() || v.Len() == 0 143 | default: 144 | return v.Interface() == reflect.Zero(reflect.TypeOf(v.Interface())).Interface() 145 | } 146 | } 147 | 148 | func areFieldsValueEqualValue(f string, obj any, value any) bool { 149 | 150 | field := reflect.Indirect(reflect.ValueOf(obj)).FieldByName(f) 151 | 152 | if value == nil { 153 | return isFieldValueZero(f, obj) 154 | } 155 | 156 | v2 := reflect.ValueOf(value) 157 | 158 | // This is to handle time structure whatever their timezone 159 | if field.Type() == reflect.ValueOf(time.Now()).Type() { 160 | return field.Interface().(time.Time).Unix() == v2.Interface().(time.Time).Unix() 161 | } 162 | 163 | if field.Kind() == reflect.Slice || field.Kind() == reflect.Array { 164 | if field.Len() != v2.Len() { 165 | return false 166 | } 167 | 168 | return reflect.DeepEqual(field.Interface(), v2.Interface()) 169 | } 170 | 171 | if field.Kind() == reflect.Map { 172 | return reflect.DeepEqual(field.Interface(), v2.Interface()) 173 | } 174 | 175 | return field.Interface() == v2.Interface() 176 | } 177 | -------------------------------------------------------------------------------- /atomicjob_test.go: -------------------------------------------------------------------------------- 1 | package elemental 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestAtomicJob(t *testing.T) { 14 | 15 | Convey("Given I call a wrapped job once and it works", t, func() { 16 | 17 | var counter int64 18 | 19 | f := AtomicJob(func() error { 20 | time.Sleep(time.Second) 21 | atomic.AddInt64(&counter, 1) 22 | return nil 23 | }) 24 | 25 | err := f(context.Background()) 26 | 27 | Convey("Then err should be nil", func() { 28 | So(err, ShouldBeNil) 29 | }) 30 | 31 | Convey("Then the job should have been executed", func() { 32 | So(atomic.LoadInt64(&counter), ShouldEqual, 1) 33 | }) 34 | }) 35 | 36 | Convey("Given I call a wrapped job once and it fails", t, func() { 37 | 38 | var counter int64 39 | 40 | f := AtomicJob(func() error { 41 | time.Sleep(time.Second) 42 | atomic.AddInt64(&counter, 1) 43 | return fmt.Errorf("oh noes") 44 | }) 45 | 46 | err := f(context.Background()) 47 | 48 | Convey("Then err should be nil", func() { 49 | So(err, ShouldNotBeNil) 50 | So(err.Error(), ShouldEqual, "oh noes") 51 | }) 52 | 53 | Convey("Then the job should have been executed", func() { 54 | So(atomic.LoadInt64(&counter), ShouldEqual, 1) 55 | }) 56 | }) 57 | 58 | Convey("Given I call a wrapped job but context is canceled", t, func() { 59 | 60 | var counter int64 61 | 62 | f := AtomicJob(func() error { 63 | time.Sleep(time.Second) 64 | atomic.AddInt64(&counter, 1) 65 | return nil 66 | }) 67 | 68 | ctx, cancel := context.WithCancel(context.Background()) 69 | cancel() 70 | 71 | err := f(ctx) 72 | 73 | Convey("Then err should not be nil", func() { 74 | So(err, ShouldNotBeNil) 75 | So(err.Error(), ShouldEqual, "context canceled") 76 | }) 77 | 78 | Convey("Then the job should have been executed", func() { 79 | So(atomic.LoadInt64(&counter), ShouldEqual, 0) 80 | }) 81 | }) 82 | 83 | Convey("Given I call a wrapped job but context cancels in the middle", t, func() { 84 | 85 | var counter int64 86 | 87 | f := AtomicJob(func() error { 88 | time.Sleep(3 * time.Second) 89 | atomic.AddInt64(&counter, 1) 90 | return nil 91 | }) 92 | 93 | ctx, cancel := context.WithCancel(context.Background()) 94 | go func() { 95 | time.Sleep(300 * time.Millisecond) 96 | cancel() 97 | }() 98 | 99 | err := f(ctx) 100 | 101 | Convey("Then err should not be nil", func() { 102 | So(err, ShouldNotBeNil) 103 | So(err.Error(), ShouldEqual, "context canceled") 104 | }) 105 | 106 | Convey("Then the job should have been executed", func() { 107 | So(atomic.LoadInt64(&counter), ShouldEqual, 0) 108 | }) 109 | }) 110 | 111 | Convey("Given I call a wrapped job twice and it fails", t, func() { 112 | 113 | var counter int64 114 | 115 | f := AtomicJob(func() error { 116 | time.Sleep(time.Second) 117 | atomic.AddInt64(&counter, 1) 118 | return fmt.Errorf("boom") 119 | }) 120 | 121 | var err1 atomic.Value 122 | 123 | ready := make(chan struct{}) 124 | go func() { 125 | ready <- struct{}{} 126 | err1.Store(f(context.Background())) 127 | }() 128 | 129 | // wait for the first trigger to be running 130 | select { 131 | case <-ready: 132 | case <-time.After(1 * time.Second): 133 | panic("job did not trigger in time") 134 | } 135 | 136 | // invoke a second marker trigger 137 | err2 := f(context.Background()) 138 | 139 | time.Sleep(300 * time.Millisecond) 140 | 141 | Convey("Then err1 and err2 should not be nil", func() { 142 | e := err1.Load().(error) // nolint:revive,unchecked-type-assertion 143 | So(e, ShouldNotBeNil) 144 | So(e.Error(), ShouldEqual, "boom") 145 | 146 | So(err2, ShouldNotBeNil) 147 | So(err2.Error(), ShouldEqual, "boom") 148 | }) 149 | 150 | Convey("Then the job should have been executed", func() { 151 | So(atomic.LoadInt64(&counter), ShouldEqual, 1) 152 | }) 153 | }) 154 | 155 | Convey("Given I call a wrapped job a lot in parallel", t, func() { 156 | 157 | var counter int64 158 | f := AtomicJob(func() error { 159 | time.Sleep(time.Second) 160 | atomic.AddInt64(&counter, 1) 161 | return nil 162 | }) 163 | 164 | var err1 atomic.Value 165 | 166 | ready := make(chan struct{}) 167 | go func() { 168 | ready <- struct{}{} 169 | if e := f(context.Background()); e != nil { 170 | err1.Store(e) 171 | } 172 | }() 173 | 174 | // wait for the first trigger to be running 175 | select { 176 | case <-ready: 177 | case <-time.After(1 * time.Second): 178 | panic("job did not trigger in time") 179 | } 180 | 181 | // invoke 100 other random trigger 182 | for i := 0; i < 100; i++ { 183 | go f(context.Background()) // nolint 184 | } 185 | 186 | // invoke a second marker trigger 187 | err2 := f(context.Background()) 188 | 189 | time.Sleep(300 * time.Millisecond) 190 | 191 | Convey("Then err1 and err2 should be nil", func() { 192 | So(err1.Load(), ShouldBeNil) 193 | So(err2, ShouldBeNil) 194 | }) 195 | 196 | Convey("Then the job should have been executed", func() { 197 | So(atomic.LoadInt64(&counter), ShouldEqual, 1) 198 | }) 199 | }) 200 | } 201 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "encoding/json" 16 | "fmt" 17 | "time" 18 | ) 19 | 20 | // EventType is the type of an event. 21 | type EventType string 22 | 23 | const ( 24 | // EventCreate is the type of creation events. 25 | EventCreate EventType = "create" 26 | 27 | // EventUpdate is the type of update events. 28 | EventUpdate EventType = "update" 29 | 30 | // EventDelete is the type of delete events. 31 | EventDelete EventType = "delete" 32 | 33 | // EventError is the type of error events. 34 | EventError EventType = "error" 35 | ) 36 | 37 | // An Event represents a computational event. 38 | type Event struct { 39 | RawData []byte `msgpack:"entity" json:"-"` 40 | JSONData json.RawMessage `msgpack:"-" json:"entity"` 41 | Identity string `msgpack:"identity" json:"identity"` 42 | Type EventType `msgpack:"type" json:"type"` 43 | Timestamp time.Time `msgpack:"timestamp" json:"timestamp"` 44 | Encoding EncodingType `msgpack:"encoding" json:"encoding"` 45 | } 46 | 47 | // NewEvent returns a new Event. 48 | func NewEvent(t EventType, o Identifiable) *Event { 49 | return NewEventWithEncoding(t, o, EncodingTypeMSGPACK) 50 | } 51 | 52 | // NewErrorEvent returns a new (error) Event embedded with the provided elemental.Error 53 | func NewErrorEvent(ee Error, encoding EncodingType) *Event { 54 | 55 | data, err := Encode(encoding, ee) 56 | if err != nil { 57 | panic(fmt.Sprintf("unable to create new error event: %s", err)) 58 | } 59 | 60 | event := &Event{ 61 | Type: EventError, 62 | Timestamp: time.Now(), 63 | Encoding: encoding, 64 | } 65 | 66 | event.configureData(encoding, data) 67 | return event 68 | } 69 | 70 | // NewEventWithEncoding returns a new Event using the given encoding 71 | func NewEventWithEncoding(t EventType, o Identifiable, encoding EncodingType) *Event { 72 | 73 | data, err := Encode(encoding, o) 74 | if err != nil { 75 | panic(fmt.Sprintf("unable to create new event: %s", err)) 76 | } 77 | 78 | event := &Event{ 79 | Type: t, 80 | Identity: o.Identity().Name, 81 | Timestamp: time.Now(), 82 | Encoding: encoding, 83 | } 84 | 85 | event.configureData(encoding, data) 86 | return event 87 | } 88 | 89 | func (e *Event) configureData(encoding EncodingType, data []byte) { 90 | switch encoding { 91 | case EncodingTypeJSON: 92 | e.JSONData = json.RawMessage(data) 93 | case EncodingTypeMSGPACK: 94 | e.RawData = data 95 | } 96 | } 97 | 98 | // GetEncoding returns the encoding used to encode the entity. 99 | func (e *Event) GetEncoding() EncodingType { 100 | return e.Encoding 101 | } 102 | 103 | // Decode decodes the data into the given destination. 104 | func (e *Event) Decode(dst any) error { 105 | return Decode(e.GetEncoding(), e.Entity(), dst) 106 | } 107 | 108 | // Convert converts the internal encoded data to the given 109 | // encoding. 110 | func (e *Event) Convert(encoding EncodingType) error { 111 | 112 | switch e.Encoding { 113 | 114 | case encoding: 115 | return nil 116 | 117 | case EncodingTypeMSGPACK: 118 | 119 | d, err := Convert(e.Encoding, encoding, e.RawData) 120 | if err != nil { 121 | return err 122 | } 123 | e.JSONData = json.RawMessage(d) 124 | e.RawData = nil 125 | 126 | default: 127 | 128 | d, err := Convert(e.Encoding, encoding, []byte(e.JSONData)) 129 | if err != nil { 130 | return err 131 | } 132 | e.JSONData = nil 133 | e.RawData = d 134 | } 135 | 136 | e.Encoding = encoding 137 | 138 | return nil 139 | } 140 | 141 | // Entity returns the byte encoded entity. 142 | func (e *Event) Entity() []byte { 143 | 144 | switch e.Encoding { 145 | case EncodingTypeMSGPACK: 146 | return e.RawData 147 | default: 148 | return []byte(e.JSONData) 149 | } 150 | } 151 | 152 | func (e *Event) String() string { 153 | 154 | return fmt.Sprintf("", e.Type, e.Identity) 155 | } 156 | 157 | // Duplicate creates a copy of the event. 158 | func (e *Event) Duplicate() *Event { 159 | 160 | var jd json.RawMessage 161 | var rd []byte 162 | 163 | if e.JSONData != nil { 164 | jd = append(json.RawMessage{}, e.JSONData...) 165 | } 166 | 167 | if e.RawData != nil { 168 | rd = append([]byte{}, e.RawData...) 169 | } 170 | 171 | return &Event{ 172 | Type: e.Type, 173 | JSONData: jd, 174 | RawData: rd, 175 | Identity: e.Identity, 176 | Timestamp: e.Timestamp, 177 | Encoding: e.Encoding, 178 | } 179 | } 180 | 181 | // An Events represents a list of Event. 182 | type Events []*Event 183 | 184 | // NewEvents retutns a new Events. 185 | func NewEvents(events ...*Event) Events { 186 | 187 | return append(Events{}, events...) 188 | } 189 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/converter_helpers_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/go-test/deep" 13 | "go.aporeto.io/regolithe/spec" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | type testCase struct { 18 | inSpec string 19 | inSkipPrivateModels bool 20 | inSplitOutput bool 21 | outDocs map[string]string 22 | supportingSpecs []string // other dependency specs needed for test case(s) 23 | } 24 | 25 | type testCaseRunner struct { 26 | t *testing.T 27 | rootTmpDir string 28 | } 29 | 30 | func runAllTestCases(t *testing.T, cases map[string]testCase) { 31 | t.Helper() 32 | 33 | rootTmpDir := t.TempDir() 34 | 35 | tcRunner := &testCaseRunner{ 36 | t: t, 37 | rootTmpDir: rootTmpDir, 38 | } 39 | for name, testCase := range cases { 40 | tcRunner.run(name, testCase) 41 | } 42 | } 43 | 44 | // Run will execute the given testcase in parallel with any other test cases 45 | func (r *testCaseRunner) run(name string, tc testCase) { 46 | r.t.Helper() 47 | 48 | r.t.Run(name, func(t *testing.T) { 49 | t.Helper() 50 | t.Parallel() 51 | 52 | testDataFiles := map[string]string{ 53 | // these files are needed by regolithe to parse the raw model from the test case 54 | "regolithe.ini": regolitheINI, 55 | "_type.mapping": typeMapping, 56 | } 57 | 58 | for i, rawSpec := range append([]string{tc.inSpec}, tc.supportingSpecs...) { 59 | rawSpec = replaceTrailingTabsWithDoubleSpaceForYAML(rawSpec) 60 | 61 | // this is needed because the spec filename has to match the rest_name of the spec model 62 | var spec struct { 63 | Model struct { 64 | RESTName string `yaml:"rest_name"` 65 | } 66 | } 67 | if err := yaml.Unmarshal([]byte(rawSpec), &spec); err != nil { 68 | t.Fatalf("error unmarshaling test spec data [%d] to read key 'rest_name': %v", i, err) 69 | } 70 | testDataFiles[spec.Model.RESTName+".spec"] = rawSpec 71 | } 72 | 73 | // this is to ensure that each test case executed within this runner is isolated 74 | specDir, err := os.MkdirTemp(r.rootTmpDir, name) 75 | if err != nil { 76 | t.Fatalf("error creating temporary directory for test case: %v", err) 77 | } 78 | 79 | for filename, content := range testDataFiles { 80 | filename = filepath.Join(specDir, filename) 81 | if err := os.WriteFile(filename, []byte(content), os.ModePerm); err != nil { 82 | t.Fatalf("error writing temporary file '%s': %v", filename, err) 83 | } 84 | } 85 | 86 | spec, err := spec.LoadSpecificationSet(specDir, nil, nil, "openapi3") 87 | if err != nil { 88 | t.Fatalf("error parsing spec set from test data: %v", err) 89 | } 90 | 91 | cfg := Config{ 92 | Public: tc.inSkipPrivateModels, 93 | SplitOutput: tc.inSplitOutput, 94 | } 95 | converter := newConverter(spec, cfg) 96 | 97 | output := map[string]*writeCloserMem{} 98 | writerFactory := func(docName string) (io.WriteCloser, error) { 99 | wr := &writeCloserMem{new(bytes.Buffer)} 100 | output[docName] = wr 101 | return wr, nil 102 | } 103 | if err := converter.Do(writerFactory); err != nil { 104 | t.Fatalf("error converting spec to openapi3: %v", err) 105 | } 106 | 107 | if la, le := len(output), len(tc.outDocs); la != le { 108 | t.Fatalf("expected %d output documents, got: %d", le, la) 109 | } 110 | for docName := range tc.outDocs { 111 | if _, ok := output[docName]; !ok { 112 | t.Fatalf("document with name '%s' does not exist in the actual output", docName) 113 | } 114 | } 115 | 116 | for expectedDocName, expectedRawDoc := range tc.outDocs { 117 | actualRawDoc := output[expectedDocName] 118 | actual := make(map[string]any) 119 | if err := json.Unmarshal(actualRawDoc.Bytes(), &actual); err != nil { 120 | t.Fatalf("invalid actual output data: malformed json content: %v", err) 121 | } 122 | 123 | expected := make(map[string]any) 124 | if err := json.Unmarshal([]byte(expectedRawDoc), &expected); err != nil { 125 | t.Fatalf("invalid expected output data in test case: malformed json content: %v", err) 126 | } 127 | 128 | if diff := deep.Equal(actual, expected); diff != nil { 129 | t.Fatal("actual != expected output\n", strings.Join(diff, "\n")) 130 | } 131 | } 132 | }) 133 | } 134 | 135 | func replaceTrailingTabsWithDoubleSpaceForYAML(s string) string { 136 | 137 | sb := new(strings.Builder) 138 | 139 | replaceNextTab := true 140 | for _, r := range s { 141 | 142 | if r == '\n' { 143 | // nolint: revive 144 | sb.WriteRune(r) 145 | replaceNextTab = true 146 | continue 147 | } 148 | 149 | if replaceNextTab && r == '\t' { 150 | // nolint: revive 151 | sb.WriteString(" ") 152 | continue 153 | } 154 | 155 | // nolint: revive 156 | sb.WriteRune(r) 157 | replaceNextTab = false 158 | } 159 | 160 | return sb.String() 161 | } 162 | 163 | type fakeWriter struct { 164 | wrErr error 165 | cErr error 166 | } 167 | 168 | func (fw *fakeWriter) Write([]byte) (int, error) { 169 | return 0, fw.wrErr 170 | } 171 | 172 | func (fw *fakeWriter) Close() error { 173 | return fw.cErr 174 | } 175 | 176 | type writeCloserMem struct { 177 | *bytes.Buffer 178 | } 179 | 180 | func (wr *writeCloserMem) Close() error { 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "encoding/json" 16 | "fmt" 17 | "net/http" 18 | "strings" 19 | ) 20 | 21 | // IsErrorWithCode returns true if the given error is an elemental.Error 22 | // or elemental.Errors with the status set to the given code. 23 | func IsErrorWithCode(err error, code int) bool { 24 | 25 | var c int 26 | switch e := err.(type) { 27 | case Error: 28 | c = e.Code 29 | case Errors: 30 | c = e.Code() 31 | } 32 | 33 | return c == code 34 | } 35 | 36 | // An Error represents a computational error. 37 | // 38 | // They can be encoded and sent back to the clients. 39 | type Error struct { 40 | Code int `msgpack:"code" json:"code,omitempty"` 41 | Description string `msgpack:"description" json:"description"` 42 | Subject string `msgpack:"subject" json:"subject"` 43 | Title string `msgpack:"title" json:"title"` 44 | Data any `msgpack:"data" json:"data,omitempty"` 45 | Trace string `msgpack:"trace" json:"trace,omitempty"` 46 | } 47 | 48 | // NewError returns a new Error. 49 | func NewError(title, description, subject string, code int) Error { 50 | 51 | return NewErrorWithData(title, description, subject, code, nil) 52 | } 53 | 54 | // NewErrorWithData returns a new Error with the given opaque data. 55 | func NewErrorWithData(title, description, subject string, code int, data any) Error { 56 | 57 | return Error{ 58 | Code: code, 59 | Description: description, 60 | Subject: subject, 61 | Title: title, 62 | Data: data, 63 | } 64 | } 65 | 66 | func (e Error) Error() string { 67 | 68 | if e.Trace != "" { 69 | return fmt.Sprintf("error %d (%s): %s: %s [trace: %s]", e.Code, e.Subject, e.Title, e.Description, e.Trace) 70 | } 71 | 72 | return fmt.Sprintf("error %d (%s): %s: %s", e.Code, e.Subject, e.Title, e.Description) 73 | } 74 | 75 | // Errors represents a list of Error. 76 | type Errors []Error 77 | 78 | // NewErrors creates a new Errors. 79 | func NewErrors(errors ...error) Errors { 80 | 81 | out := Errors{} 82 | if len(errors) == 0 { 83 | return out 84 | } 85 | 86 | return out.Append(errors...) 87 | } 88 | 89 | func (e Errors) Error() string { 90 | 91 | strs := make([]string, len(e)) 92 | 93 | for i := range e { 94 | strs[i] = e[i].Error() 95 | } 96 | 97 | return strings.Join(strs, ", ") 98 | } 99 | 100 | // Code returns the code of the first error code in the Errors. 101 | func (e Errors) Code() int { 102 | 103 | if len(e) == 0 { 104 | return -1 105 | } 106 | 107 | return e[0].Code 108 | } 109 | 110 | // Append returns returns a copy of the receiver containing 111 | // also the given errors. 112 | func (e Errors) Append(errs ...error) Errors { 113 | 114 | out := append(Errors{}, e...) 115 | 116 | for _, err := range errs { 117 | switch er := err.(type) { 118 | case Error: 119 | out = append(out, er) 120 | case Errors: 121 | out = append(out, er...) 122 | default: 123 | out = append(out, NewError("Internal Server Error", err.Error(), "elemental", http.StatusInternalServerError)) 124 | } 125 | } 126 | 127 | return out 128 | } 129 | 130 | // Trace returns Errors with all inside Error marked with the 131 | // given trace ID. 132 | func (e Errors) Trace(id string) Errors { 133 | 134 | out := Errors{} 135 | 136 | for _, err := range e { 137 | err.Trace = id 138 | out = append(out, err) 139 | } 140 | 141 | return out 142 | } 143 | 144 | // DecodeErrors decodes the given bytes into a en elemental.Errors. 145 | func DecodeErrors(data []byte) (Errors, error) { 146 | 147 | es := []Error{} 148 | if err := json.Unmarshal(data, &es); err != nil { 149 | return nil, err 150 | } 151 | 152 | e := NewErrors() 153 | for _, err := range es { 154 | e = append(e, err) 155 | } 156 | 157 | return e, nil 158 | } 159 | 160 | // IsValidationError returns true if the given error is a validation error 161 | // with the given title for the given attribute. 162 | func IsValidationError(err error, title string, attribute string) bool { 163 | 164 | var elementalError Error 165 | switch e := err.(type) { 166 | 167 | case Errors: 168 | if e.Code() != http.StatusUnprocessableEntity { 169 | return false 170 | } 171 | if len(e) != 1 { 172 | return false 173 | } 174 | elementalError = e[0] 175 | 176 | case Error: 177 | if e.Code != http.StatusUnprocessableEntity { 178 | return false 179 | } 180 | elementalError = e 181 | 182 | default: 183 | return false 184 | } 185 | 186 | if elementalError.Title != title { 187 | return false 188 | } 189 | 190 | if elementalError.Data == nil { 191 | return false 192 | } 193 | 194 | m, ok := elementalError.Data.(map[string]any) 195 | if !ok { 196 | return false 197 | } 198 | 199 | return m["attribute"].(string) == attribute 200 | } 201 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/gen_converter.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | "go.aporeto.io/regolithe/spec" 12 | ) 13 | 14 | const ( 15 | paramNameID = "id" 16 | defaultDocName = "toplevel" 17 | ) 18 | 19 | type converter struct { 20 | skipPrivateModels bool 21 | splitOutput bool 22 | inSpecSet spec.SpecificationSet 23 | resourceToRest map[string]string 24 | tagsForModel map[string]openapi3.Tags 25 | globalTagSet map[string]*openapi3.Tag 26 | outRootDoc openapi3.T 27 | } 28 | 29 | func newConverter(inSpecSet spec.SpecificationSet, cfg Config) *converter { 30 | c := &converter{ 31 | skipPrivateModels: cfg.Public, 32 | splitOutput: cfg.SplitOutput, 33 | inSpecSet: inSpecSet, 34 | resourceToRest: make(map[string]string), 35 | tagsForModel: make(map[string]openapi3.Tags), 36 | globalTagSet: make(map[string]*openapi3.Tag), 37 | outRootDoc: newOpenAPI3Template(inSpecSet.Configuration()), 38 | } 39 | 40 | for _, specif := range inSpecSet.Specifications() { 41 | model := specif.Model() 42 | c.resourceToRest[model.ResourceName] = model.RestName 43 | } 44 | 45 | return c 46 | } 47 | 48 | func (c *converter) Do(newWriter func(name string) (io.WriteCloser, error)) error { 49 | 50 | for _, s := range c.inSpecSet.Specifications() { 51 | if err := c.processSpec(s); err != nil { 52 | return fmt.Errorf("unable to to process spec: %w", err) 53 | } 54 | c.cacheTags(s.Model()) 55 | } 56 | 57 | for name, doc := range c.convertedDocs() { 58 | dest, err := newWriter(name) 59 | if err != nil { 60 | return fmt.Errorf("'%s': unable to create write destination: %w", name, err) 61 | } 62 | 63 | enc := json.NewEncoder(dest) 64 | enc.SetIndent("", " ") 65 | if err := enc.Encode(doc); err != nil { 66 | return fmt.Errorf("'%s': marshaling openapi3 document: %w", name, err) 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (c *converter) processSpec(s spec.Specification) error { 74 | 75 | model := s.Model() 76 | 77 | if c.skipPrivateModels && model.Private { 78 | return nil 79 | } 80 | 81 | if model.IsRoot { 82 | pathItems := c.convertRelationsForRootSpec(s.Relations()) 83 | for path, item := range pathItems { 84 | c.outRootDoc.Paths[path] = item 85 | } 86 | // we don't care about root model's relations for now, so we are done for root spec 87 | return nil 88 | } 89 | 90 | schema, err := c.convertModel(s) 91 | if err != nil { 92 | return fmt.Errorf("model '%s': %w", model.RestName, err) 93 | } 94 | c.outRootDoc.Components.Schemas[model.RestName] = schema 95 | 96 | pathItems := c.convertRelationsForNonRootModel(model) 97 | for path, item := range pathItems { 98 | c.outRootDoc.Paths[path] = item 99 | } 100 | 101 | pathItems = c.convertRelationsForNonRootSpec(model.ResourceName, s.Relations()) 102 | for path, item := range pathItems { 103 | c.outRootDoc.Paths[path] = item 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (c *converter) convertedDocs() map[string]openapi3.T { 110 | 111 | if !c.splitOutput || len(c.outRootDoc.Components.Schemas) == 0 { 112 | c.outRootDoc.Tags = c.globalTags() 113 | return map[string]openapi3.T{defaultDocName: c.outRootDoc} 114 | } 115 | 116 | docs := make(map[string]openapi3.T) 117 | specConfig := c.inSpecSet.Configuration() 118 | for name, schema := range c.outRootDoc.Components.Schemas { 119 | template := newOpenAPI3Template(specConfig) 120 | template.Components.Schemas[name] = schema 121 | template.Info.Title = name 122 | template.Tags = c.tagsForModel[name] 123 | docs[name] = template 124 | } 125 | 126 | for path, item := range c.outRootDoc.Paths { 127 | pathRoot := strings.SplitN(strings.Trim(path, "/"), "/", 2)[0] 128 | docName := c.resourceToRest[pathRoot] 129 | docs[docName].Paths[path] = item 130 | } 131 | 132 | return docs 133 | } 134 | 135 | func (c *converter) cacheTags(model *spec.Model) { 136 | 137 | if model.IsRoot { 138 | return 139 | } 140 | if c.skipPrivateModels && model.Private { 141 | return 142 | } 143 | 144 | tags := openapi3.Tags{ 145 | { 146 | Name: model.Group, 147 | Description: fmt.Sprintf("This tag is for group '%s'", model.Group), 148 | }, 149 | { 150 | Name: model.Package, 151 | Description: fmt.Sprintf("This tag is for package '%s'", model.Package), 152 | }, 153 | } 154 | 155 | for _, t := range tags { 156 | c.globalTagSet[t.Name] = t 157 | } 158 | c.tagsForModel[model.RestName] = tags 159 | } 160 | 161 | func (c *converter) globalTags() openapi3.Tags { 162 | tags := make(openapi3.Tags, 0, len(c.globalTagSet)) 163 | for _, t := range c.globalTagSet { 164 | tags = append(tags, t) 165 | } 166 | sort.Slice(tags, func(i, j int) bool { 167 | return tags[i].Name < tags[j].Name 168 | }) 169 | return tags 170 | } 171 | 172 | func newOpenAPI3Template(specConfig *spec.Config) openapi3.T { 173 | return openapi3.T{ 174 | OpenAPI: "3.0.3", 175 | Info: &openapi3.Info{ 176 | Title: defaultDocName, 177 | Version: specConfig.Version, 178 | Description: specConfig.Description, 179 | TermsOfService: "https://localhost/TODO", // TODO 180 | License: &openapi3.License{ 181 | Name: "TODO", 182 | }, 183 | Contact: &openapi3.Contact{ 184 | Name: specConfig.Author, 185 | URL: specConfig.URL, 186 | Email: specConfig.Email, 187 | }, 188 | }, 189 | Paths: openapi3.Paths{}, 190 | Components: &openapi3.Components{ 191 | Schemas: make(openapi3.Schemas), 192 | }, 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "fmt" 16 | "net/http" 17 | "reflect" 18 | ) 19 | 20 | const ( 21 | readOnlyErrorTitle = "Read Only Error" 22 | creationOnlyErrorTitle = "Creation Only Error" 23 | ) 24 | 25 | // ValidateAdvancedSpecification verifies advanced specifications attributes like ReadOnly and CreationOnly. 26 | // 27 | // For instance, it will check if the given Manipulable has field marked as 28 | // readonly, that it has not changed according to the db. 29 | func ValidateAdvancedSpecification(obj AttributeSpecifiable, pristine AttributeSpecifiable, op Operation) error { 30 | 31 | errors := NewErrors() 32 | 33 | for _, field := range extractFieldNames(obj) { 34 | 35 | spec := obj.SpecificationForAttribute(field) 36 | 37 | // If the field is not exposed, we don't enforce anything. 38 | if !spec.Exposed || spec.Transient { 39 | continue 40 | } 41 | 42 | switch op { 43 | case OperationCreate: 44 | if spec.ReadOnly && !isFieldValueZero(field, obj) && !areFieldsValueEqualValue(field, obj, spec.DefaultValue) { 45 | 46 | // Special case here. If we have a pristine object, and the fields are equal, it is fine. 47 | if pristine != nil && areFieldValuesEqual(field, obj, pristine) { 48 | continue 49 | } 50 | 51 | e := NewError( 52 | readOnlyErrorTitle, 53 | fmt.Sprintf("Field %s is read only. You cannot set its value.", spec.Name), 54 | "elemental", 55 | http.StatusUnprocessableEntity, 56 | ) 57 | e.Data = map[string]string{"attribute": spec.Name} 58 | errors = append(errors, e) 59 | } 60 | 61 | case OperationUpdate: 62 | if !spec.CreationOnly && spec.ReadOnly && !areFieldValuesEqual(field, obj, pristine) { 63 | e := NewError( 64 | readOnlyErrorTitle, 65 | fmt.Sprintf("Field %s is read only. You cannot modify its value.", spec.Name), 66 | "elemental", 67 | http.StatusUnprocessableEntity, 68 | ) 69 | e.Data = map[string]string{"attribute": spec.Name} 70 | errors = append(errors, e) 71 | } 72 | 73 | if spec.CreationOnly && !areFieldValuesEqual(field, obj, pristine) { 74 | e := NewError( 75 | creationOnlyErrorTitle, 76 | fmt.Sprintf("Field %s can only be set during creation. You cannot modify its value.", spec.Name), 77 | "elemental", 78 | http.StatusUnprocessableEntity, 79 | ) 80 | e.Data = map[string]string{"attribute": spec.Name} 81 | errors = append(errors, e) 82 | } 83 | } 84 | } 85 | 86 | if len(errors) > 0 { 87 | return errors 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // BackportUnexposedFields copy the values of unexposed fields from src to dest. 94 | func BackportUnexposedFields(src, dest AttributeSpecifiable) { 95 | 96 | for _, field := range extractFieldNames(src) { 97 | 98 | spec := src.SpecificationForAttribute(field) 99 | 100 | if !spec.Exposed { 101 | reflect.Indirect(reflect.ValueOf(dest)).FieldByName(field).Set(reflect.Indirect(reflect.ValueOf(src)).FieldByName(field)) 102 | } 103 | 104 | if spec.Secret && isFieldValueZero(field, dest) { 105 | reflect.Indirect(reflect.ValueOf(dest)).FieldByName(field).Set(reflect.Indirect(reflect.ValueOf(src)).FieldByName(field)) 106 | } 107 | } 108 | } 109 | 110 | // ResetDefaultForZeroValues reset the default value from the specification when a field is Zero. 111 | // If the given object is not an elemental.AttributeSpecifiable this function 112 | // does nothing. 113 | func ResetDefaultForZeroValues(obj any) { 114 | 115 | o, ok := obj.(AttributeSpecifiable) 116 | if !ok { 117 | return 118 | } 119 | 120 | for _, field := range extractFieldNames(o) { 121 | 122 | spec := o.SpecificationForAttribute(field) 123 | 124 | if spec.DefaultValue == nil || !isFieldValueZero(field, o) { 125 | continue 126 | } 127 | 128 | reflect.Indirect(reflect.ValueOf(o)).FieldByName(field).Set(reflect.ValueOf(spec.DefaultValue)) 129 | } 130 | } 131 | 132 | // ResetMaps recursively empty all kinds of maps in the given 133 | // reflect.Value. 134 | func ResetMaps(v reflect.Value) { 135 | 136 | indirect := func(vv reflect.Value) reflect.Value { 137 | for ; vv.Kind() == reflect.Ptr; vv = vv.Elem() { 138 | _ = vv // this makes the linter happy as the loop is not empty 139 | } 140 | return vv 141 | } 142 | 143 | v = indirect(v) 144 | 145 | if !v.IsValid() { 146 | return 147 | } 148 | 149 | reset := func(f reflect.Value) { 150 | 151 | switch f.Kind() { 152 | case reflect.Map: 153 | 154 | if f.IsNil() { 155 | return 156 | } 157 | 158 | for _, k := range f.MapKeys() { 159 | f.SetMapIndex(k, reflect.Value{}) 160 | } 161 | 162 | case reflect.Struct, reflect.Slice: 163 | ResetMaps(f) 164 | } 165 | } 166 | 167 | switch v.Kind() { 168 | 169 | case reflect.Map: 170 | reset(v) 171 | 172 | case reflect.Slice: 173 | for i := 0; i < v.Len(); i++ { 174 | reset(indirect(v.Index(i))) 175 | } 176 | 177 | case reflect.Struct: 178 | for i := 0; i < v.NumField(); i++ { 179 | reset(indirect(v.Field(i))) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /identity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import "fmt" 15 | 16 | // An IdentifiablesList is a list of objects implementing the Identifiable interface. 17 | type IdentifiablesList []Identifiable 18 | 19 | // Identifiables is the interface of a list of Identifiable that can 20 | // returns the Identity of the objects it contains. 21 | type Identifiables interface { 22 | Identity() Identity 23 | List() IdentifiablesList 24 | Copy() Identifiables 25 | Append(...Identifiable) Identifiables 26 | Versionable 27 | } 28 | 29 | // A PlainIdentifiables is the interface of an object that can return a sparse 30 | // version of itself. 31 | type PlainIdentifiables interface { 32 | 33 | // ToSparse returns a sparsed version of the object. 34 | ToSparse(...string) Identifiables 35 | 36 | Identifiables 37 | } 38 | 39 | // A SparseIdentifiables is the interface of an object that can return a full 40 | // version of itself. 41 | type SparseIdentifiables interface { 42 | 43 | // ToPlain returns the full version of the object. 44 | ToPlain() IdentifiablesList 45 | 46 | Identifiables 47 | } 48 | 49 | // An Identifiable is the interface that Elemental objects must implement. 50 | type Identifiable interface { 51 | 52 | // Identity returns the Identity of the of the receiver. 53 | Identity() Identity 54 | 55 | // Identifier returns the unique identifier of the of the receiver. 56 | Identifier() string 57 | 58 | // SetIdentifier sets the unique identifier of the of the receiver. 59 | SetIdentifier(string) 60 | 61 | Versionable 62 | } 63 | 64 | // A PlainIdentifiable is the interface of an object that can return a sparse 65 | // version of itself. 66 | type PlainIdentifiable interface { 67 | 68 | // ToSparse returns a sparsed version of the object. 69 | ToSparse(...string) SparseIdentifiable 70 | 71 | Identifiable 72 | } 73 | 74 | // A SparseIdentifiable is the interface of an object that can return a full 75 | // version of itself. 76 | type SparseIdentifiable interface { 77 | 78 | // ToPlain returns the full version of the object. 79 | ToPlain() PlainIdentifiable 80 | 81 | Identifiable 82 | } 83 | 84 | // DefaultOrderer is the interface of an object that has default ordering fields. 85 | type DefaultOrderer interface { 86 | 87 | // Default order returns the keys that can be used for default ordering. 88 | DefaultOrder() []string 89 | } 90 | 91 | // AttributeEncryptable is the interface of on object that 92 | // has encryptable 93 | type AttributeEncryptable interface { 94 | EncryptAttributes(encrypter AttributeEncrypter) error 95 | DecryptAttributes(encrypter AttributeEncrypter) error 96 | } 97 | 98 | // An Identity is a structure that contains the necessary information about an Identifiable. 99 | // The Name is usually the singular form of the Category. 100 | // 101 | // For instance, "cat" and "cats". 102 | type Identity struct { 103 | Name string `msgpack:"name" json:"name"` 104 | Category string `msgpack:"category" json:"category"` 105 | Private bool `msgpack:"-" json:"-"` 106 | Package string `msgpack:"-" json:"-"` 107 | } 108 | 109 | // MakeIdentity returns a new Identity. 110 | func MakeIdentity(name, category string) Identity { 111 | 112 | return Identity{ 113 | Name: name, 114 | Category: category, 115 | } 116 | } 117 | 118 | // String returns the string representation of the identity. 119 | func (i Identity) String() string { 120 | 121 | return fmt.Sprintf("", i.Name, i.Category) 122 | } 123 | 124 | // IsEmpty checks if the identity is empty. 125 | func (i Identity) IsEmpty() bool { 126 | 127 | return i.Name == "" && i.Category == "" 128 | } 129 | 130 | // IsEqual checks if the given identity is equal to the receiver. 131 | func (i Identity) IsEqual(identity Identity) bool { 132 | 133 | return i.Name == identity.Name && i.Category == identity.Category 134 | } 135 | 136 | // AllIdentity represents all possible Identities. 137 | var AllIdentity = Identity{ 138 | Name: "*", 139 | Category: "*", 140 | } 141 | 142 | // EmptyIdentity represents an empty Identity. 143 | var EmptyIdentity = Identity{ 144 | Name: "", 145 | Category: "", 146 | } 147 | 148 | // RootIdentity represents an root Identity. 149 | var RootIdentity = Identity{ 150 | Name: "root", 151 | Category: "root", 152 | } 153 | 154 | // A Documentable is an object that can be documented. 155 | type Documentable interface { 156 | Doc() string 157 | } 158 | 159 | // A Versionable is an object that can be versioned. 160 | type Versionable interface { 161 | Version() int 162 | } 163 | 164 | // A Patchable the interface of an object that can be patched. 165 | type Patchable interface { 166 | 167 | // Patch patches the receiver using the given SparseIdentifiable. 168 | Patch(SparseIdentifiable) 169 | } 170 | 171 | // A Namespaceable is the interface of an object that is namespaced. 172 | type Namespaceable interface { 173 | GetNamespace() string 174 | SetNamespace(string) 175 | } 176 | 177 | // A Propagatable is the interface of an object that can propagate down 178 | // from a parent namespace. 179 | type Propagatable interface { 180 | GetProgagate() bool 181 | Namespaceable 182 | } 183 | -------------------------------------------------------------------------------- /test/model/relationships_registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by elegen. DO NOT EDIT. 2 | // Source: go.aporeto.io/elemental (templates/relationships_registry.gotpl) 3 | 4 | package testmodel 5 | 6 | import "go.aporeto.io/elemental" 7 | 8 | var relationshipsRegistry elemental.RelationshipsRegistry 9 | 10 | func init() { 11 | 12 | relationshipsRegistry = elemental.RelationshipsRegistry{} 13 | 14 | relationshipsRegistry[ListIdentity] = &elemental.Relationship{ 15 | Create: map[string]*elemental.RelationshipInfo{ 16 | "root": { 17 | Parameters: []elemental.ParameterDefinition{ 18 | { 19 | Name: "rlcp1", 20 | Type: "string", 21 | }, 22 | { 23 | Name: "rlcp2", 24 | Type: "boolean", 25 | }, 26 | }, 27 | }, 28 | }, 29 | Update: map[string]*elemental.RelationshipInfo{ 30 | "root": { 31 | Parameters: []elemental.ParameterDefinition{ 32 | { 33 | Name: "lup1", 34 | Type: "string", 35 | }, 36 | { 37 | Name: "lup2", 38 | Type: "boolean", 39 | }, 40 | }, 41 | }, 42 | }, 43 | Patch: map[string]*elemental.RelationshipInfo{ 44 | "root": { 45 | Parameters: []elemental.ParameterDefinition{ 46 | { 47 | Name: "lup1", 48 | Type: "string", 49 | }, 50 | { 51 | Name: "lup2", 52 | Type: "boolean", 53 | }, 54 | }, 55 | }, 56 | }, 57 | Delete: map[string]*elemental.RelationshipInfo{ 58 | "root": { 59 | Parameters: []elemental.ParameterDefinition{ 60 | { 61 | Name: "ldp1", 62 | Type: "string", 63 | }, 64 | { 65 | Name: "ldp2", 66 | Type: "boolean", 67 | }, 68 | }, 69 | }, 70 | }, 71 | Retrieve: map[string]*elemental.RelationshipInfo{ 72 | "root": { 73 | Parameters: []elemental.ParameterDefinition{ 74 | { 75 | Name: "lgp1", 76 | Type: "string", 77 | }, 78 | { 79 | Name: "lgp2", 80 | Type: "boolean", 81 | }, 82 | { 83 | Name: "sAp1", 84 | Type: "string", 85 | }, 86 | { 87 | Name: "sAp2", 88 | Type: "boolean", 89 | }, 90 | { 91 | Name: "sBp1", 92 | Type: "string", 93 | }, 94 | { 95 | Name: "sBp2", 96 | Type: "boolean", 97 | }, 98 | }, 99 | }, 100 | }, 101 | RetrieveMany: map[string]*elemental.RelationshipInfo{ 102 | "root": { 103 | Parameters: []elemental.ParameterDefinition{ 104 | { 105 | Name: "rlgmp1", 106 | Type: "string", 107 | }, 108 | { 109 | Name: "rlgmp2", 110 | Type: "boolean", 111 | }, 112 | }, 113 | }, 114 | }, 115 | Info: map[string]*elemental.RelationshipInfo{ 116 | "root": { 117 | Parameters: []elemental.ParameterDefinition{ 118 | { 119 | Name: "rlgmp1", 120 | Type: "string", 121 | }, 122 | { 123 | Name: "rlgmp2", 124 | Type: "boolean", 125 | }, 126 | }, 127 | }, 128 | }, 129 | } 130 | 131 | relationshipsRegistry[RootIdentity] = &elemental.Relationship{} 132 | 133 | relationshipsRegistry[TaskIdentity] = &elemental.Relationship{ 134 | Create: map[string]*elemental.RelationshipInfo{ 135 | "list": { 136 | Parameters: []elemental.ParameterDefinition{ 137 | { 138 | Name: "ltcp1", 139 | Type: "string", 140 | }, 141 | { 142 | Name: "ltcp2", 143 | Type: "boolean", 144 | }, 145 | }, 146 | }, 147 | }, 148 | Update: map[string]*elemental.RelationshipInfo{ 149 | "root": {}, 150 | }, 151 | Patch: map[string]*elemental.RelationshipInfo{ 152 | "root": {}, 153 | }, 154 | Delete: map[string]*elemental.RelationshipInfo{ 155 | "root": {}, 156 | }, 157 | Retrieve: map[string]*elemental.RelationshipInfo{ 158 | "root": {}, 159 | }, 160 | RetrieveMany: map[string]*elemental.RelationshipInfo{ 161 | "list": { 162 | Parameters: []elemental.ParameterDefinition{ 163 | { 164 | Name: "ltgp1", 165 | Type: "string", 166 | }, 167 | { 168 | Name: "ltgp2", 169 | Type: "boolean", 170 | }, 171 | }, 172 | }, 173 | }, 174 | Info: map[string]*elemental.RelationshipInfo{ 175 | "list": { 176 | Parameters: []elemental.ParameterDefinition{ 177 | { 178 | Name: "ltgp1", 179 | Type: "string", 180 | }, 181 | { 182 | Name: "ltgp2", 183 | Type: "boolean", 184 | }, 185 | }, 186 | }, 187 | }, 188 | } 189 | 190 | relationshipsRegistry[UserIdentity] = &elemental.Relationship{ 191 | Create: map[string]*elemental.RelationshipInfo{ 192 | "root": { 193 | Parameters: []elemental.ParameterDefinition{ 194 | { 195 | Name: "rucp1", 196 | Type: "string", 197 | }, 198 | { 199 | Name: "rucp2", 200 | Type: "boolean", 201 | }, 202 | }, 203 | }, 204 | }, 205 | Update: map[string]*elemental.RelationshipInfo{ 206 | "root": {}, 207 | }, 208 | Patch: map[string]*elemental.RelationshipInfo{ 209 | "root": {}, 210 | }, 211 | Delete: map[string]*elemental.RelationshipInfo{ 212 | "root": { 213 | RequiredParameters: elemental.NewParametersRequirement( 214 | [][][]string{ 215 | { 216 | { 217 | "confirm", 218 | }, 219 | }, 220 | }, 221 | ), 222 | Parameters: []elemental.ParameterDefinition{ 223 | { 224 | Name: "confirm", 225 | Type: "boolean", 226 | }, 227 | }, 228 | }, 229 | }, 230 | Retrieve: map[string]*elemental.RelationshipInfo{ 231 | "root": {}, 232 | }, 233 | RetrieveMany: map[string]*elemental.RelationshipInfo{ 234 | "list": {}, 235 | "root": { 236 | Parameters: []elemental.ParameterDefinition{ 237 | { 238 | Name: "rugmp1", 239 | Type: "string", 240 | }, 241 | { 242 | Name: "rugmp2", 243 | Type: "boolean", 244 | }, 245 | }, 246 | }, 247 | }, 248 | Info: map[string]*elemental.RelationshipInfo{ 249 | "list": {}, 250 | "root": { 251 | Parameters: []elemental.ParameterDefinition{ 252 | { 253 | Name: "rugmp1", 254 | Type: "string", 255 | }, 256 | { 257 | Name: "rugmp2", 258 | Type: "boolean", 259 | }, 260 | }, 261 | }, 262 | }, 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /cmd/elegen/templates/identities_registry.gotpl: -------------------------------------------------------------------------------- 1 | // Code generated by elegen. DO NOT EDIT. 2 | // Source: go.aporeto.io/elemental (templates/identities_registry.gotpl) 3 | 4 | package {{ .Set.Configuration.Name }} 5 | 6 | import "go.aporeto.io/elemental" 7 | 8 | var ( 9 | identityNamesMap = map[string]elemental.Identity { 10 | {{- range .Set.Specifications }} 11 | {{ if shouldRegisterSpecification . $.PublicMode }} 12 | {{- $entityName := .Model.EntityName -}} 13 | "{{.Model.RestName}}": {{ $entityName }}Identity, 14 | {{- end }} 15 | {{- end }} 16 | } 17 | 18 | identitycategoriesMap = map[string]elemental.Identity { 19 | {{- range .Set.Specifications }} 20 | {{ if shouldRegisterSpecification . $.PublicMode }} 21 | {{- $entityName := .Model.EntityName -}} 22 | "{{.Model.ResourceName}}": {{ $entityName }}Identity, 23 | {{- end }} 24 | {{- end }} 25 | } 26 | 27 | aliasesMap = map[string]elemental.Identity { 28 | {{- range .Set.Specifications }} 29 | {{- if shouldRegisterSpecification . $.PublicMode }} 30 | {{- $entityName := .Model.EntityName -}} 31 | {{ range $i, $alias := .Model.Aliases }} 32 | "{{ $alias }}": {{ $entityName }}Identity, 33 | {{- end }} 34 | {{- end }} 35 | {{- end }} 36 | } 37 | 38 | indexesMap = map[string][][]string { 39 | {{- range .Set.Specifications }} 40 | {{- if shouldRegisterSpecification . $.PublicMode }} 41 | "{{ .Model.RestName }}": {{ if len .Indexes }} { 42 | {{- range $i, $compound := sortIndexes .Indexes }} 43 | { {{- range $compound }}"{{ . }}",{{- end }} }, 44 | {{- end }} 45 | },{{ else }}nil,{{- end }} 46 | {{- end }} 47 | {{- end }} 48 | } 49 | ) 50 | 51 | // ModelVersion returns the current version of the model. 52 | func ModelVersion() float64 { return {{ .Set.APIInfo.Version }} } 53 | 54 | type modelManager struct{} 55 | 56 | func(f modelManager) IdentityFromName(name string) elemental.Identity { 57 | 58 | return identityNamesMap[name] 59 | } 60 | 61 | func(f modelManager) IdentityFromCategory(category string) elemental.Identity { 62 | 63 | return identitycategoriesMap[category] 64 | } 65 | 66 | func(f modelManager) IdentityFromAlias(alias string) elemental.Identity { 67 | 68 | return aliasesMap[alias] 69 | } 70 | 71 | func(f modelManager) IdentityFromAny(any string) (i elemental.Identity) { 72 | 73 | if i = f.IdentityFromName(any); !i.IsEmpty() { 74 | return i 75 | } 76 | 77 | if i = f.IdentityFromCategory(any); !i.IsEmpty() { 78 | return i 79 | } 80 | 81 | return f.IdentityFromAlias(any) 82 | } 83 | 84 | func(f modelManager) Identifiable(identity elemental.Identity) elemental.Identifiable { 85 | 86 | switch identity { 87 | {{ range .Set.Specifications }} 88 | {{- if shouldRegisterSpecification . $.PublicMode }} 89 | case {{ .Model.EntityName }}Identity: 90 | return New{{ .Model.EntityName }}() 91 | {{- end }} 92 | {{- end }} 93 | default: 94 | return nil 95 | } 96 | } 97 | 98 | func(f modelManager) SparseIdentifiable(identity elemental.Identity) elemental.SparseIdentifiable { 99 | 100 | switch identity { 101 | {{ range .Set.Specifications }} 102 | {{- if shouldRegisterSpecification . $.PublicMode }} 103 | {{- if not .Model.IsRoot }} 104 | case {{ .Model.EntityName }}Identity: 105 | return NewSparse{{ .Model.EntityName }}() 106 | {{- end }} 107 | {{- end }} 108 | {{- end }} 109 | default: 110 | return nil 111 | } 112 | } 113 | 114 | func(f modelManager) Indexes(identity elemental.Identity) [][]string { 115 | 116 | return indexesMap[identity.Name] 117 | } 118 | 119 | func(f modelManager) IdentifiableFromString(any string) elemental.Identifiable { 120 | 121 | return f.Identifiable(f.IdentityFromAny(any)) 122 | } 123 | 124 | func(f modelManager) Identifiables(identity elemental.Identity) elemental.Identifiables { 125 | 126 | switch identity { 127 | {{ range .Set.Specifications }} 128 | {{- if not .Model.IsRoot }} 129 | {{- if shouldRegisterSpecification . $.PublicMode }} 130 | case {{ .Model.EntityName }}Identity: 131 | return &{{ .Model.EntityNamePlural }}List{} 132 | {{- end }} 133 | {{- end }} 134 | {{- end }} 135 | default: 136 | return nil 137 | } 138 | } 139 | 140 | func(f modelManager) SparseIdentifiables(identity elemental.Identity) elemental.SparseIdentifiables { 141 | 142 | switch identity { 143 | {{ range .Set.Specifications }} 144 | {{- if not .Model.IsRoot }} 145 | {{- if shouldRegisterSpecification . $.PublicMode }} 146 | case {{ .Model.EntityName }}Identity: 147 | return &Sparse{{ .Model.EntityNamePlural }}List{} 148 | {{- end }} 149 | {{- end }} 150 | {{- end }} 151 | default: 152 | return nil 153 | } 154 | } 155 | 156 | func(f modelManager) IdentifiablesFromString(any string) elemental.Identifiables { 157 | 158 | return f.Identifiables(f.IdentityFromAny(any)) 159 | } 160 | 161 | func(f modelManager) Relationships() elemental.RelationshipsRegistry { 162 | 163 | return relationshipsRegistry 164 | } 165 | 166 | func (f modelManager) AllIdentities() []elemental.Identity { 167 | return AllIdentities() 168 | } 169 | 170 | var manager = modelManager{} 171 | 172 | // Manager returns the model elemental.ModelManager. 173 | func Manager() elemental.ModelManager { return manager } 174 | 175 | 176 | // AllIdentities returns all existing identities. 177 | func AllIdentities() []elemental.Identity { 178 | 179 | return []elemental.Identity{ 180 | {{- range .Set.Specifications }} 181 | {{- if shouldRegisterSpecification . $.PublicMode }} 182 | {{ .Model.EntityName }}Identity, 183 | {{- end }} 184 | {{- end }} 185 | } 186 | } 187 | 188 | // AliasesForIdentity returns all the aliases for the given identity. 189 | func AliasesForIdentity(identity elemental.Identity) []string { 190 | 191 | switch identity { 192 | {{- range .Set.Specifications }} 193 | {{- if shouldRegisterSpecification . $.PublicMode }} 194 | case {{ .Model.EntityName }}Identity: 195 | return []string{ {{ range $i, $alias := .Model.Aliases }} 196 | "{{ $alias }}", 197 | {{- end }} 198 | } 199 | {{- end }} 200 | {{- end }} 201 | } 202 | 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /cmd/elegen/writers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "go/format" 18 | "go/scanner" 19 | "path" 20 | "strings" 21 | "text/template" 22 | 23 | "go.aporeto.io/regolithe/spec" 24 | "golang.org/x/text/cases" 25 | "golang.org/x/text/language" 26 | "golang.org/x/tools/imports" 27 | ) 28 | 29 | var functions = template.FuncMap{ 30 | "upper": strings.ToUpper, 31 | "lower": strings.ToLower, 32 | "capitalize": cases.Title(language.Und, cases.NoLower).String, 33 | "join": strings.Join, 34 | "hasPrefix": strings.HasPrefix, 35 | "attrBSONFieldName": attrBSONFieldName, 36 | "attrToType": attrToType, 37 | "attrToField": attrToField, 38 | "attrToMongoField": attrToMongoField, 39 | "escBackticks": escapeBackticks, 40 | "buildEnums": buildEnums, 41 | "shouldGenerateGetter": shouldGenerateGetter, 42 | "shouldGenerateSetter": shouldGenerateSetter, 43 | "shouldWriteInitializer": shouldWriteInitializer, 44 | "shouldWriteAttributeMap": shouldWriteAttributeMap, 45 | "shouldRegisterSpecification": shouldRegisterSpecification, 46 | "shouldRegisterRelationship": shouldRegisterRelationship, 47 | "shouldRegisterInnerRelationship": shouldRegisterInnerRelationship, 48 | "writeInitializer": writeInitializer, 49 | "writeDefaultValue": writeDefaultValue, 50 | "sortAttributes": sortAttributes, 51 | "sortIndexes": sortIndexes, 52 | "modelCommentFlags": modelCommentFlags, 53 | } 54 | 55 | func writeModel(set spec.SpecificationSet, name string, outFolder string, publicMode bool) error { 56 | 57 | tmpl, err := makeTemplate("templates/model.gotpl") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | s := set.Specification(name) 63 | 64 | bnames := map[string]struct{}{} 65 | for _, attr := range s.Attributes(s.LatestAttributesVersion()) { 66 | item, ok := attr.Extensions["bson_name"] 67 | if !ok { 68 | continue 69 | } 70 | bname, ok := item.(string) 71 | if !ok { 72 | return fmt.Errorf("expected string for extension attribute 'bson_name', got %T instead", item) 73 | } 74 | if _, ok = bnames[bname]; ok { 75 | return fmt.Errorf("invalid bson name. '%s' reused", bname) 76 | } 77 | bnames[bname] = struct{}{} 78 | } 79 | 80 | if s.Model().Private && publicMode { 81 | return nil 82 | } 83 | 84 | var buf bytes.Buffer 85 | 86 | if err = tmpl.Execute( 87 | &buf, 88 | struct { 89 | PublicMode bool 90 | Set spec.SpecificationSet 91 | Spec spec.Specification 92 | }{ 93 | PublicMode: publicMode, 94 | Set: set, 95 | Spec: s, 96 | }); err != nil { 97 | return fmt.Errorf("unable to generate model '%s': %s", name, err) 98 | } 99 | 100 | p, err := format.Source(buf.Bytes()) 101 | if err != nil { 102 | if errs, ok := err.(scanner.ErrorList); ok { 103 | lines := strings.Split(buf.String(), "\n") 104 | for i := 0; i < errs.Len(); i++ { 105 | _, err2 := fmt.Printf("Error in '%s' near:\n\n\t%s\n\n", name, lines[errs[i].Pos.Line-1]) 106 | if err2 != nil { 107 | return fmt.Errorf("unable to print error in formatting model (name: %s): %s", name, err2) 108 | } 109 | } 110 | } 111 | return fmt.Errorf("unable to format model '%s': %s", name, err) 112 | } 113 | 114 | p, err = imports.Process("", p, nil) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | if err := writeFile(path.Join(outFolder, name+".go"), p); err != nil { 120 | return fmt.Errorf("unable to write file for spec: %s", name) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func writeIdentitiesRegistry(set spec.SpecificationSet, outFolder string, publicMode bool) error { 127 | 128 | tmpl, err := makeTemplate("templates/identities_registry.gotpl") 129 | if err != nil { 130 | return err 131 | } 132 | 133 | var buf bytes.Buffer 134 | 135 | if err = tmpl.Execute( 136 | &buf, 137 | struct { 138 | PublicMode bool 139 | Set spec.SpecificationSet 140 | }{ 141 | PublicMode: publicMode, 142 | Set: set, 143 | }); err != nil { 144 | return fmt.Errorf("unable to generate identities_registry code:%s", err) 145 | } 146 | 147 | p, err := format.Source(buf.Bytes()) 148 | if err != nil { 149 | return fmt.Errorf("unable to format identities_registry code:%s", err) 150 | } 151 | 152 | p, err = imports.Process("", p, nil) 153 | if err != nil { 154 | _, err2 := fmt.Println(buf.String()) 155 | if err2 != nil { 156 | return fmt.Errorf("unable to print error in goimport relationships_registry logic: %s", err2) 157 | } 158 | return fmt.Errorf("unable to goimport relationships_registry code:%s", err) 159 | } 160 | 161 | if err := writeFile(path.Join(outFolder, "identities_registry.go"), p); err != nil { 162 | return fmt.Errorf("unable to write file for identities_registry: %s", err) 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func writeRelationshipsRegistry(set spec.SpecificationSet, outFolder string, publicMode bool) error { 169 | 170 | tmpl, err := makeTemplate("templates/relationships_registry.gotpl") 171 | if err != nil { 172 | return err 173 | } 174 | 175 | var buf bytes.Buffer 176 | 177 | if err = tmpl.Execute( 178 | &buf, 179 | struct { 180 | PublicMode bool 181 | Set spec.SpecificationSet 182 | }{ 183 | PublicMode: publicMode, 184 | Set: set, 185 | }); err != nil { 186 | return fmt.Errorf("unable to generate relationships_registry code:%s", err) 187 | } 188 | 189 | p, err := format.Source(buf.Bytes()) 190 | if err != nil { 191 | _, err2 := fmt.Println(buf.String()) 192 | if err2 != nil { 193 | return fmt.Errorf("unable to print error in formatting relationships_registry logic: %s", err2) 194 | } 195 | return fmt.Errorf("unable to format relationships_registry code:%s", err) 196 | } 197 | 198 | p, err = imports.Process("", p, nil) 199 | if err != nil { 200 | _, err2 := fmt.Println(buf.String()) 201 | if err2 != nil { 202 | return fmt.Errorf("unable to print error in relationships_registry logic: %s", err2) 203 | } 204 | return fmt.Errorf("unable to goimport relationships_registry code:%s", err) 205 | } 206 | 207 | if err := writeFile(path.Join(outFolder, "relationships_registry.go"), p); err != nil { 208 | return fmt.Errorf("unable to write file for relationships_registry: %s", err) 209 | } 210 | 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /push_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "fmt" 16 | "net/url" 17 | "slices" 18 | ) 19 | 20 | // A PushFilter represents an abstract filter for filtering out push notifications. This is now aliased to PushConfig as a 21 | // result of re-naming the type. 22 | // 23 | // Deprecated: use the new name PushConfig instead 24 | type PushFilter = PushConfig 25 | 26 | // A PushConfig represents an abstract filter for filtering out push notifications. 27 | // 28 | // The 'IdentityFilters' field is a mapping between a filtered identity and the string representation of an elemental.Filter. 29 | // A client will supply this attribute if they want fine-grained filtering on the set of identities that they are filtering on. 30 | // If this attribute has been supplied, the identities passed to 'IdentityFilters' must be a subset of the identities passed to 31 | // 'Identities'; passing in identities that are not provided in the 'Identities' field will be ignored. 32 | type PushConfig struct { 33 | Identities map[string][]EventType `msgpack:"identities" json:"identities"` 34 | IdentityFilters map[string]string `msgpack:"filters" json:"filters"` 35 | Params url.Values `msgpack:"parameters" json:"parameters"` 36 | 37 | // parsedIdentityFilters holds the parsed `IdentityFilters` to avoid re-parsing the configured filters on each push 38 | // event that is using the same config. 39 | parsedIdentityFilters map[string]*Filter 40 | } 41 | 42 | // unsupportedComparators is the list of comparators that are currently not handled by the elemental API 'MatchesFilter' 43 | // which is utilized for providing fine-grained identity filtering for websocket clients 44 | var unsupportedComparators = []FilterComparator{ 45 | GreaterComparator, 46 | GreaterOrEqualComparator, 47 | LesserComparator, 48 | LesserOrEqualComparator, 49 | InComparator, 50 | NotInComparator, 51 | ContainComparator, 52 | NotContainComparator, 53 | } 54 | 55 | // NewPushFilter returns a new PushFilter. NewPushFilter is now aliased to NewPushConfig. This was done for backwards 56 | // compatibility as a result of the re-naming of PushFilter to PushConfig. 57 | // 58 | // Deprecated: use the constructor with the new name, NewPushConfig, instead 59 | func NewPushFilter() *PushFilter { 60 | // nolint: revive 61 | fmt.Println("DEPRECATED: elemental.NewPushFilter is deprecated, use elemental.NewPushConfig instead") 62 | 63 | return &PushFilter{ 64 | Identities: map[string][]EventType{}, 65 | IdentityFilters: map[string]string{}, 66 | parsedIdentityFilters: map[string]*Filter{}, 67 | } 68 | } 69 | 70 | // NewPushConfig returns a new PushConfig. 71 | func NewPushConfig() *PushConfig { 72 | 73 | return &PushConfig{ 74 | Identities: map[string][]EventType{}, 75 | IdentityFilters: map[string]string{}, 76 | parsedIdentityFilters: map[string]*Filter{}, 77 | } 78 | } 79 | 80 | // SetParameter sets the values of the parameter with the given key. 81 | func (pc *PushConfig) SetParameter(key string, values ...string) { 82 | 83 | if pc.Params == nil { 84 | pc.Params = url.Values{} 85 | } 86 | 87 | pc.Params[key] = values 88 | } 89 | 90 | // Parameters returns a copy of all the parameters. 91 | func (pc *PushConfig) Parameters() url.Values { 92 | 93 | if pc.Params == nil { 94 | return nil 95 | } 96 | 97 | out := make(url.Values, len(pc.Params)) 98 | for k, v := range pc.Params { 99 | out[k] = slices.Clone(v) 100 | } 101 | 102 | return out 103 | } 104 | 105 | // FilterIdentity adds the given identity for the given eventTypes in the PushConfig. 106 | func (pc *PushConfig) FilterIdentity(identityName string, eventTypes ...EventType) { 107 | 108 | pc.Identities[identityName] = eventTypes 109 | } 110 | 111 | // ParseIdentityFilters parses the configured PushConfig's 'IdentityFilters' attribute to elemental filters. 112 | // The parsed filters will then be stored in the non-exposed 'parsedIdentityFilters' attribute of PushConfig. This is useful 113 | // for clients that wish the utilize the same filter multiple times without having to incur the overhead of parsing each time. 114 | // 115 | // An error is returned in following situations: 116 | // - when a filter is declared on an identity that is not defined in the PushConfig's 'Identities' attribute 117 | // - when a filter cannot be parsed into an elemental.Filter 118 | func (pc *PushConfig) ParseIdentityFilters() error { 119 | 120 | if pc.parsedIdentityFilters == nil { 121 | pc.parsedIdentityFilters = map[string]*Filter{} 122 | } 123 | 124 | for identity, unparsedFilter := range pc.IdentityFilters { 125 | if _, found := pc.Identities[identity]; !found { 126 | // in the event an error occurs we zero out the parsed identities to avoid having a partially set of parsed identities 127 | pc.parsedIdentityFilters = map[string]*Filter{} 128 | return fmt.Errorf("elemental: cannot declare an identity filter on %q as that was not declared in 'Identities'", identity) 129 | } 130 | 131 | filter, err := NewFilterParser(unparsedFilter, 132 | // blacklist unsupported comparators so socket can either be closed or if the client supports error events, an 133 | // error can be emitted. 134 | OptUnsupportedComparators(unsupportedComparators), 135 | ).Parse() 136 | if err != nil { 137 | // in the event an error occurs we zero out the parsed identities to avoid having a partially set of parsed identities 138 | pc.parsedIdentityFilters = map[string]*Filter{} 139 | return fmt.Errorf("elemental: unable to parse filter %q: %s", unparsedFilter, err) 140 | } 141 | 142 | pc.parsedIdentityFilters[identity] = filter 143 | } 144 | 145 | return nil 146 | } 147 | 148 | // IsFilteredOut returns true if the given Identity is not part of the PushConfig's Identity mapping 149 | func (pc *PushConfig) IsFilteredOut(identityName string, eventType EventType) bool { 150 | 151 | // if the identities list is empty, we filter nothing. 152 | if len(pc.Identities) == 0 { 153 | return false 154 | } 155 | 156 | // If it contains something, but not the identity, we filter out. 157 | types, ok := pc.Identities[identityName] 158 | if !ok { 159 | return true 160 | } 161 | 162 | // If there is no event types defined we don't filter 163 | if len(types) == 0 { 164 | return false 165 | } 166 | 167 | // If if there are some event types defined, we don't filter out 168 | // if the current event type is in the list. 169 | for _, t := range types { 170 | if t == eventType { 171 | return false 172 | } 173 | } 174 | 175 | return true 176 | } 177 | 178 | // FilterForIdentity returns the associated fine-grained filter for the given identity. In the event that no fine-grained 179 | // filter has been configured for the identity, the second return value (a boolean), will be set to false. 180 | func (pc *PushConfig) FilterForIdentity(identityName string) (*Filter, bool) { 181 | 182 | if pc.parsedIdentityFilters == nil { 183 | return nil, false 184 | } 185 | 186 | filter, found := pc.parsedIdentityFilters[identityName] 187 | return filter, found 188 | } 189 | 190 | // Duplicate duplicates the PushConfig. 191 | func (pc *PushConfig) Duplicate() *PushConfig { 192 | 193 | config := NewPushConfig() 194 | 195 | for id, types := range pc.Identities { 196 | config.FilterIdentity(id, types...) 197 | } 198 | 199 | for id, f := range pc.IdentityFilters { 200 | config.IdentityFilters[id] = f 201 | } 202 | 203 | for id, f := range pc.parsedIdentityFilters { 204 | config.parsedIdentityFilters[id] = f 205 | } 206 | 207 | for k, v := range pc.Params { 208 | config.SetParameter(k, v...) 209 | } 210 | 211 | return config 212 | } 213 | 214 | func (pc *PushConfig) String() string { 215 | 216 | return fmt.Sprintf("", pc.Identities, pc.IdentityFilters) 217 | } 218 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/gen_relations.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "go.aporeto.io/regolithe/spec" 8 | ) 9 | 10 | var noDesc = "n/a" 11 | 12 | func (c *converter) convertRelationsForRootSpec(relations []*spec.Relation) map[string]*openapi3.PathItem { 13 | 14 | paths := make(map[string]*openapi3.PathItem) 15 | 16 | for _, relation := range relations { 17 | 18 | model := relation.Specification().Model() 19 | 20 | if c.skipPrivateModels && model.Private { 21 | continue 22 | } 23 | 24 | if relation.Get == nil && relation.Create == nil { 25 | continue 26 | } 27 | 28 | pathItem := &openapi3.PathItem{ 29 | Get: c.extractOperationGetAll("", relation), 30 | Post: c.extractOperationPost("", relation), 31 | } 32 | 33 | uri := "/" + model.ResourceName 34 | paths[uri] = pathItem 35 | } 36 | 37 | return paths 38 | } 39 | 40 | func (c *converter) convertRelationsForNonRootSpec(resourceName string, relations []*spec.Relation) map[string]*openapi3.PathItem { 41 | 42 | paths := make(map[string]*openapi3.PathItem) 43 | parentRestName := c.resourceToRest[resourceName] 44 | 45 | for _, relation := range relations { 46 | 47 | if relation.Get == nil && relation.Create == nil { 48 | continue 49 | } 50 | 51 | pathItem := &openapi3.PathItem{ 52 | Get: c.extractOperationGetAll(parentRestName, relation), 53 | Post: c.extractOperationPost(parentRestName, relation), 54 | } 55 | 56 | c.insertParamID(&pathItem.Parameters) 57 | 58 | childModel := c.inSpecSet.Specification(relation.RestName).Model() 59 | uri := fmt.Sprintf("/%s/{%s}/%s", resourceName, paramNameID, childModel.ResourceName) 60 | paths[uri] = pathItem 61 | } 62 | 63 | return paths 64 | } 65 | 66 | func (c *converter) convertRelationsForNonRootModel(model *spec.Model) map[string]*openapi3.PathItem { 67 | 68 | if model.Get == nil && model.Update == nil && model.Delete == nil { 69 | return nil 70 | } 71 | 72 | pathItem := &openapi3.PathItem{ 73 | Get: c.extractOperationGetByID(model), 74 | Delete: c.extractOperationDeleteByID(model), 75 | Put: c.extractOperationPutByID(model), 76 | } 77 | c.insertParamID(&pathItem.Parameters) 78 | 79 | uri := fmt.Sprintf("/%s/{%s}", model.ResourceName, paramNameID) 80 | pathItems := map[string]*openapi3.PathItem{uri: pathItem} 81 | return pathItems 82 | } 83 | 84 | func (c *converter) extractOperationGetAll(parentRestName string, relation *spec.Relation) *openapi3.Operation { 85 | 86 | if relation == nil || relation.Get == nil { 87 | return nil 88 | } 89 | relationAction := relation.Get 90 | 91 | model := relation.Specification().Model() 92 | 93 | respBodySchema := openapi3.NewArraySchema() 94 | if !c.splitOutput || parentRestName == "" { 95 | respBodySchema.Items = openapi3.NewSchemaRef("#/components/schemas/"+model.RestName, nil) 96 | } else { 97 | respBodySchema.Items = openapi3.NewSchemaRef("./"+model.RestName+"#/components/schemas/"+model.RestName, nil) 98 | } 99 | 100 | op := &openapi3.Operation{ 101 | OperationID: "get-all-" + model.ResourceName, 102 | Tags: []string{model.Group, model.Package}, 103 | Description: relationAction.Description, 104 | Responses: openapi3.Responses{ 105 | "200": &openapi3.ResponseRef{ 106 | Value: &openapi3.Response{ 107 | Description: &noDesc, 108 | Content: openapi3.Content{ 109 | "application/json": &openapi3.MediaType{ 110 | Schema: respBodySchema.NewRef(), 111 | }, 112 | }, 113 | }, 114 | }, 115 | // TODO: more responses like 422, 500, etc if needed 116 | }, 117 | Parameters: c.convertParamDefAsQueryParams(relationAction.ParameterDefinition), 118 | } 119 | 120 | if parentRestName != "" { 121 | op.OperationID = "get-all-" + model.ResourceName + "-for-a-given-" + parentRestName 122 | } 123 | return op 124 | } 125 | 126 | func (c *converter) extractOperationPost(parentRestName string, relation *spec.Relation) *openapi3.Operation { 127 | 128 | if relation == nil || relation.Create == nil { 129 | return nil 130 | } 131 | relationAction := relation.Create 132 | 133 | model := relation.Specification().Model() 134 | 135 | var schemaRef *openapi3.SchemaRef 136 | 137 | if !c.splitOutput || parentRestName == "" { 138 | schemaRef = openapi3.NewSchemaRef("#/components/schemas/"+relation.RestName, nil) 139 | } else { 140 | schemaRef = openapi3.NewSchemaRef("./"+relation.RestName+"#/components/schemas/"+relation.RestName, nil) 141 | } 142 | 143 | op := &openapi3.Operation{ 144 | OperationID: "create-a-new-" + relation.RestName, 145 | Tags: []string{model.Group, model.Package}, 146 | Description: relationAction.Description, 147 | RequestBody: &openapi3.RequestBodyRef{ 148 | Value: &openapi3.RequestBody{ 149 | Content: openapi3.Content{ 150 | "application/json": &openapi3.MediaType{ 151 | Schema: schemaRef, 152 | }, 153 | }, 154 | }, 155 | }, 156 | Responses: openapi3.Responses{ 157 | "200": &openapi3.ResponseRef{ 158 | Value: &openapi3.Response{ 159 | Description: &noDesc, 160 | Content: openapi3.Content{ 161 | "application/json": &openapi3.MediaType{ 162 | Schema: schemaRef, 163 | }, 164 | }, 165 | }, 166 | }, 167 | // TODO: more responses like 422, 500, etc if needed 168 | }, 169 | Parameters: c.convertParamDefAsQueryParams(relationAction.ParameterDefinition), 170 | } 171 | 172 | if parentRestName != "" { 173 | op.OperationID = "create-a-new-" + model.RestName + "-for-a-given-" + parentRestName 174 | } 175 | return op 176 | } 177 | 178 | func (c *converter) extractOperationGetByID(model *spec.Model) *openapi3.Operation { 179 | 180 | if model == nil || model.Get == nil { 181 | return nil 182 | } 183 | relationAction := model.Get 184 | 185 | respBodySchemaRef := openapi3.NewSchemaRef("#/components/schemas/"+model.RestName, nil) 186 | 187 | op := &openapi3.Operation{ 188 | OperationID: fmt.Sprintf("get-%s-by-ID", model.RestName), 189 | Tags: []string{model.Group, model.Package}, 190 | Description: relationAction.Description, 191 | Responses: openapi3.Responses{ 192 | "200": &openapi3.ResponseRef{ 193 | Value: &openapi3.Response{ 194 | Description: &noDesc, 195 | Content: openapi3.Content{ 196 | "application/json": &openapi3.MediaType{ 197 | Schema: respBodySchemaRef, 198 | }, 199 | }, 200 | }, 201 | }, 202 | // TODO: more responses like 422, 500, etc if needed 203 | }, 204 | Parameters: c.convertParamDefAsQueryParams(relationAction.ParameterDefinition), 205 | } 206 | 207 | return op 208 | } 209 | 210 | func (c *converter) extractOperationDeleteByID(model *spec.Model) *openapi3.Operation { 211 | 212 | if model == nil || model.Delete == nil { 213 | return nil 214 | } 215 | relationAction := model.Delete 216 | 217 | respBodySchemaRef := openapi3.NewSchemaRef("#/components/schemas/"+model.RestName, nil) 218 | 219 | op := &openapi3.Operation{ 220 | OperationID: fmt.Sprintf("delete-%s-by-ID", model.RestName), 221 | Tags: []string{model.Group, model.Package}, 222 | Description: relationAction.Description, 223 | Responses: openapi3.Responses{ 224 | "200": &openapi3.ResponseRef{ 225 | Value: &openapi3.Response{ 226 | Description: &noDesc, 227 | Content: openapi3.Content{ 228 | "application/json": &openapi3.MediaType{ 229 | Schema: respBodySchemaRef, 230 | }, 231 | }, 232 | }, 233 | }, 234 | // TODO: more responses like 422, 500, etc if needed 235 | }, 236 | Parameters: c.convertParamDefAsQueryParams(relationAction.ParameterDefinition), 237 | } 238 | 239 | return op 240 | } 241 | 242 | func (c *converter) extractOperationPutByID(model *spec.Model) *openapi3.Operation { 243 | 244 | if model == nil || model.Update == nil { 245 | return nil 246 | } 247 | relationAction := model.Update 248 | 249 | schemaRef := openapi3.NewSchemaRef("#/components/schemas/"+model.RestName, nil) 250 | 251 | op := &openapi3.Operation{ 252 | OperationID: fmt.Sprintf("update-%s-by-ID", model.RestName), 253 | Tags: []string{model.Group, model.Package}, 254 | Description: relationAction.Description, 255 | RequestBody: &openapi3.RequestBodyRef{ 256 | Value: &openapi3.RequestBody{ 257 | Content: openapi3.Content{ 258 | "application/json": &openapi3.MediaType{ 259 | Schema: schemaRef, 260 | }, 261 | }, 262 | }, 263 | }, 264 | Responses: openapi3.Responses{ 265 | "200": &openapi3.ResponseRef{ 266 | Value: &openapi3.Response{ 267 | Description: &noDesc, 268 | Content: openapi3.Content{ 269 | "application/json": &openapi3.MediaType{ 270 | Schema: schemaRef, 271 | }, 272 | }, 273 | }, 274 | }, 275 | // TODO: more responses like 422, 500, etc if needed 276 | }, 277 | Parameters: c.convertParamDefAsQueryParams(relationAction.ParameterDefinition), 278 | } 279 | 280 | return op 281 | } 282 | -------------------------------------------------------------------------------- /attribute_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "encoding/base64" 16 | "testing" 17 | 18 | . "github.com/smartystreets/goconvey/convey" 19 | ) 20 | 21 | func TestAttribute_Interface(t *testing.T) { 22 | 23 | Convey("Given I create a new *List", t, func() { 24 | 25 | l := NewList() 26 | 27 | Convey("Then the list should implement the AttributeSpecifiable interface", func() { 28 | So(l, ShouldImplement, (*AttributeSpecifiable)(nil)) 29 | }) 30 | }) 31 | } 32 | 33 | func TestAttribute_SpecificationForAttribute(t *testing.T) { 34 | 35 | Convey("Given I create a new List", t, func() { 36 | 37 | l := NewList() 38 | 39 | Convey("When I get the Attribute specification for the name", func() { 40 | 41 | spec := l.SpecificationForAttribute("Name") 42 | 43 | Convey("then it should be correct", func() { 44 | So(spec.AllowedChars, ShouldEqual, "") 45 | So(len(spec.AllowedChoices), ShouldEqual, 0) 46 | So(spec.Autogenerated, ShouldBeFalse) 47 | So(spec.Availability, ShouldBeEmpty) 48 | So(spec.Channel, ShouldBeEmpty) 49 | So(spec.CreationOnly, ShouldBeFalse) 50 | So(spec.Deprecated, ShouldBeFalse) 51 | So(spec.Exposed, ShouldBeTrue) 52 | So(spec.Filterable, ShouldBeTrue) 53 | So(spec.ForeignKey, ShouldBeFalse) 54 | So(spec.Getter, ShouldBeTrue) 55 | So(spec.Identifier, ShouldBeFalse) 56 | So(spec.Index, ShouldBeFalse) 57 | So(spec.MaxLength, ShouldEqual, 0) 58 | So(spec.MaxValue, ShouldEqual, 0) 59 | So(spec.MinLength, ShouldEqual, 0) 60 | So(spec.MinValue, ShouldEqual, 0) 61 | So(spec.Orderable, ShouldBeTrue) 62 | So(spec.PrimaryKey, ShouldBeFalse) 63 | So(spec.ReadOnly, ShouldBeFalse) 64 | So(spec.Required, ShouldBeTrue) 65 | So(spec.Setter, ShouldBeTrue) 66 | So(spec.Stored, ShouldBeTrue) 67 | So(spec.SubType, ShouldBeEmpty) 68 | So(spec.Transient, ShouldBeFalse) 69 | So(spec.Signed, ShouldBeFalse) 70 | }) 71 | }) 72 | }) 73 | } 74 | 75 | func Test_ResetSecretAttributesValues(t *testing.T) { 76 | 77 | Convey("Given I have an identifiable with a secret property", t, func() { 78 | 79 | l := NewList() 80 | l.Secret = "it's a secret to everybody" 81 | 82 | Convey("When I call ResetSecretAttributesValues", func() { 83 | 84 | ResetSecretAttributesValues(l) 85 | 86 | Convey("Then the secret should have been erased", func() { 87 | So(l.Secret, ShouldEqual, "") 88 | }) 89 | }) 90 | }) 91 | 92 | Convey("Given I have some identifiables with a secret property", t, func() { 93 | 94 | l1 := NewList() 95 | l1.Secret = "it's a secret to everybody" 96 | 97 | l2 := NewList() 98 | l2.Secret = "it's a secret to everybody" 99 | 100 | Convey("When I call ResetSecretAttributesValues", func() { 101 | 102 | ResetSecretAttributesValues(ListsList{l1, l2}) 103 | 104 | Convey("Then the secret should have been erased", func() { 105 | So(l1.Secret, ShouldEqual, "") 106 | So(l2.Secret, ShouldEqual, "") 107 | }) 108 | }) 109 | }) 110 | 111 | Convey("Given I have an sparse identifiable with a secret property", t, func() { 112 | 113 | val := "it's a secret to everybody" 114 | 115 | l := NewSparseList() 116 | l.Secret = &val 117 | 118 | Convey("When I call ResetSecretAttributesValues", func() { 119 | 120 | ResetSecretAttributesValues(l) 121 | 122 | Convey("Then the secret should have been erased", func() { 123 | So(l.Secret, ShouldBeNil) 124 | }) 125 | }) 126 | }) 127 | 128 | Convey("Given I have some sparse identifiables with a secret property", t, func() { 129 | 130 | l1 := NewSparseList() 131 | val1 := "it's a secret to everybody" 132 | l1.Secret = &val1 133 | 134 | l2 := NewSparseList() 135 | val2 := "it's a secret to everybody" 136 | l2.Secret = &val2 137 | 138 | Convey("When I call ResetSecretAttributesValues", func() { 139 | 140 | ResetSecretAttributesValues(SparseListsList{l1, l2}) 141 | 142 | Convey("Then the secret should have been erased", func() { 143 | So(l1.Secret, ShouldBeNil) 144 | So(l2.Secret, ShouldBeNil) 145 | }) 146 | }) 147 | }) 148 | 149 | Convey("Given I have some random non pointer struct", t, func() { 150 | 151 | s := struct{}{} 152 | 153 | Convey("When I call ResetSecretAttributesValues", func() { 154 | 155 | ResetSecretAttributesValues(s) 156 | 157 | Convey("Then it should not panic", func() { 158 | So(func() { ResetSecretAttributesValues(s) }, ShouldNotPanic) 159 | }) 160 | }) 161 | }) 162 | 163 | Convey("Given I have some random pointer struct", t, func() { 164 | 165 | s := &struct{}{} 166 | 167 | Convey("Then it should not panic", func() { 168 | So(func() { ResetSecretAttributesValues(s) }, ShouldNotPanic) 169 | }) 170 | }) 171 | 172 | Convey("Given I have some nil pointer struct", t, func() { 173 | 174 | var s *struct{} 175 | 176 | Convey("Then it should not panic", func() { 177 | So(func() { ResetSecretAttributesValues(s) }, ShouldNotPanic) 178 | }) 179 | }) 180 | 181 | Convey("Given I have some nil value", t, func() { 182 | 183 | Convey("Then it should not panic", func() { 184 | So(func() { ResetSecretAttributesValues(nil) }, ShouldNotPanic) 185 | }) 186 | }) 187 | 188 | Convey("Given I have some nil pointer identifiable", t, func() { 189 | 190 | var s *List 191 | 192 | Convey("Then it should not panic", func() { 193 | So(func() { ResetSecretAttributesValues(s) }, ShouldNotPanic) 194 | }) 195 | }) 196 | 197 | Convey("Given I have an non pointer identifiable with a secret property", t, func() { 198 | 199 | l := NewList() 200 | l.Secret = "it's a secret to everybody" 201 | 202 | Convey("When I call ResetSecretAttributesValues", func() { 203 | 204 | ResetSecretAttributesValues(*l) 205 | 206 | Convey("Then the secret should have been erased", func() { 207 | So(l.Secret, ShouldEqual, "it's a secret to everybody") 208 | }) 209 | }) 210 | }) 211 | } 212 | 213 | func TestNewAESEncrypter(t *testing.T) { 214 | 215 | Convey("Given I call AESAttributeEncrypter with valid passphrase", t, func() { 216 | 217 | enc, err := NewAESAttributeEncrypter("0123456789ABCDEF") 218 | 219 | Convey("Then err should be nil", func() { 220 | So(err, ShouldBeNil) 221 | }) 222 | 223 | Convey("Then enc should be correct", func() { 224 | So(enc.(*aesAttributeEncrypter).passphrase, ShouldResemble, []byte("0123456789ABCDEF")) 225 | }) 226 | }) 227 | 228 | Convey("Given I call AESAttributeEncrypter with passphrase that is too small", t, func() { 229 | 230 | enc, err := NewAESAttributeEncrypter("0123456789ABCDE") 231 | 232 | Convey("Then err should not be nil", func() { 233 | So(err, ShouldNotBeNil) 234 | So(err.Error(), ShouldEqual, "invalid passphrase: size must be exactly 16 bytes") 235 | }) 236 | 237 | Convey("Then enc should be nil", func() { 238 | So(enc, ShouldBeNil) 239 | }) 240 | }) 241 | 242 | Convey("Given I call AESAttributeEncrypter with passphrase that is too long", t, func() { 243 | 244 | enc, err := NewAESAttributeEncrypter("0123456789ABCDE WEEEE") 245 | 246 | Convey("Then err should not be nil", func() { 247 | So(err, ShouldNotBeNil) 248 | So(err.Error(), ShouldEqual, "invalid passphrase: size must be exactly 16 bytes") 249 | }) 250 | 251 | Convey("Then enc should be nil", func() { 252 | So(enc, ShouldBeNil) 253 | }) 254 | }) 255 | } 256 | 257 | func TestAESEncrypterEncryption(t *testing.T) { 258 | 259 | Convey("Given I have an AESAttributeEncrypter ", t, func() { 260 | 261 | value := "hello world" 262 | enc, _ := NewAESAttributeEncrypter("0123456789ABCDEF") 263 | 264 | Convey("When I encrypt some data", func() { 265 | 266 | encstring, err := enc.EncryptString(value) 267 | Convey("Then err should be nil", func() { 268 | So(err, ShouldBeNil) 269 | }) 270 | 271 | b64decodeddata, err1 := base64.StdEncoding.DecodeString(encstring) 272 | Convey("Then err1 should be nil", func() { 273 | So(err1, ShouldBeNil) 274 | }) 275 | 276 | Convey("Then encstring should be encrypted", func() { 277 | So(encstring, ShouldNotEqual, value) 278 | So(b64decodeddata, ShouldNotEqual, value) 279 | }) 280 | 281 | Convey("When I decrypt the data", func() { 282 | 283 | decstring, err := enc.DecryptString(encstring) 284 | 285 | Convey("Then err should be nil", func() { 286 | So(err, ShouldBeNil) 287 | }) 288 | 289 | Convey("Then decstring should be decrypted", func() { 290 | So(decstring, ShouldEqual, value) 291 | }) 292 | }) 293 | }) 294 | 295 | Convey("When I encrypt empty string", func() { 296 | 297 | encstring, err := enc.EncryptString("") 298 | 299 | Convey("Then err should be nil", func() { 300 | So(err, ShouldBeNil) 301 | }) 302 | 303 | Convey("Then encstring should be empty", func() { 304 | So(encstring, ShouldEqual, "") 305 | }) 306 | }) 307 | 308 | Convey("When I decrypt empty string", func() { 309 | 310 | decstring, err := enc.DecryptString("") 311 | 312 | Convey("Then err should be nil", func() { 313 | So(err, ShouldBeNil) 314 | }) 315 | 316 | Convey("Then decstring should be empty", func() { 317 | So(decstring, ShouldEqual, "") 318 | }) 319 | }) 320 | 321 | Convey("When I decrypt non base64", func() { 322 | 323 | decstring, err := enc.DecryptString("1") 324 | 325 | Convey("Then err should be nil", func() { 326 | So(err, ShouldNotBeNil) 327 | So(err.Error(), ShouldEqual, "illegal base64 data at input byte 0") 328 | }) 329 | 330 | Convey("Then decstring should be empty", func() { 331 | So(decstring, ShouldEqual, "") 332 | }) 333 | }) 334 | 335 | Convey("When I decrypt too small data", func() { 336 | 337 | decstring, err := enc.DecryptString("abcd") 338 | 339 | Convey("Then err should be nil", func() { 340 | So(err, ShouldNotBeNil) 341 | So(err.Error(), ShouldEqual, "data is too small") 342 | }) 343 | 344 | Convey("Then decstring should be empty", func() { 345 | So(decstring, ShouldEqual, "") 346 | }) 347 | }) 348 | }) 349 | } 350 | -------------------------------------------------------------------------------- /attribute.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "crypto/aes" 16 | "crypto/cipher" 17 | "crypto/rand" 18 | "encoding/base64" 19 | "fmt" 20 | "io" 21 | "reflect" 22 | ) 23 | 24 | // An AttributeSpecifiable is the interface an object must implement in order to access specification of its attributes. 25 | type AttributeSpecifiable interface { 26 | 27 | // SpecificationForAttribute returns the AttributeSpecification for 28 | // given attribute name 29 | SpecificationForAttribute(string) AttributeSpecification 30 | 31 | // AttributeSpecifications returns all the AttributeSpecification mapped by 32 | // attribute name 33 | AttributeSpecifications() map[string]AttributeSpecification 34 | 35 | // ValueForAttribute returns the value for the given attribute 36 | ValueForAttribute(name string) any 37 | } 38 | 39 | // AttributeEncrypter is the interface that must be 40 | // implement to manage encrypted attributes. 41 | type AttributeEncrypter interface { 42 | 43 | // EncryptString encrypts the given string and returns the encrypted version. 44 | EncryptString(string) (string, error) 45 | 46 | // DecryptString decrypts the given string and returns the encrypted version. 47 | DecryptString(string) (string, error) 48 | } 49 | 50 | // An AttributeSpecification represents all the metadata of an attribute. 51 | // 52 | // This information is coming from the Monolithe Specifications. 53 | type AttributeSpecification struct { 54 | 55 | // AllowedChars is a regexp that will be used to validate 56 | // what value a string attribute can take. 57 | // 58 | // This is enforced by elemental. 59 | AllowedChars string 60 | 61 | // AllowedChoices is a list of possible values for an attribute. 62 | // 63 | // This is enforced by elemental. 64 | AllowedChoices []string 65 | 66 | // Autogenerated defines if the attribute is autogenerated by the server. 67 | // It can be used in conjunction with ReadOnly. 68 | // 69 | // This is not enforced by elemental. You must write your own business logic to honor this. 70 | Autogenerated bool 71 | 72 | // Availability is reserved for later use. 73 | Availability string 74 | 75 | // BSONFieldName is the name of the field that will be used when encoding/decoding the field into Binary JSON format. 76 | BSONFieldName string 77 | 78 | // ConvertedName contains the name after local conversion. 79 | ConvertedName string 80 | 81 | // Channel is reserved for later use. 82 | Channel string 83 | 84 | // CreationOnly defines if the attribute can be set only during creation. 85 | // 86 | // This is not enforced by elemental. You must write your own business logic to honor this. 87 | CreationOnly bool 88 | 89 | // DefaultValue holds the default value declared in specification. 90 | DefaultValue any 91 | 92 | // Deprecated defines if the attribute is deprecated. 93 | Deprecated bool 94 | 95 | // Description contains the description of the attribute. 96 | Description string 97 | 98 | // Exposed defines if the attribute is exposed through the north bound API. 99 | Exposed bool 100 | 101 | // Filterable defines if it is possible to filter based on this attribute. 102 | // 103 | // This is not enforced by elemental. You must write your own business logic to honor this. 104 | Filterable bool 105 | 106 | // ForeignKey defines if the attribute is a foreign key. 107 | ForeignKey bool 108 | 109 | // Getter defines if the attribute needs to define a getter method. 110 | // This is useful if you can to define an Interface based on this attribute. 111 | Getter bool 112 | 113 | // Identifier defines if the attribute is used the access key from the 114 | // northbound API. 115 | Identifier bool 116 | 117 | // Index defines if the attribute is indexed or not. 118 | // 119 | // This is not enforced by elemental. You must write your own business logic to honor this. 120 | Index bool 121 | 122 | // MaxLength defines what is the maximun length of the attribute. 123 | // This only makes sense if the type is a string. 124 | // 125 | // This is enforced by elemental. 126 | MaxLength uint 127 | 128 | // MaxValue defines what is the maximun value of the attribute. 129 | // This only makes sense if the type has a numeric type. 130 | // 131 | // This is enforced by elemental. 132 | MaxValue float64 133 | 134 | // MinLength defines what is the minimum length of the attribute. 135 | // This only makes sense if the type is a string. 136 | // 137 | // This is enforced by elemental. 138 | MinLength uint 139 | 140 | // MinValue defines what is the minimum value of the attribute. 141 | // This only makes sense if the type has a numeric type. 142 | // 143 | // This is enforced by elemental. 144 | MinValue float64 145 | 146 | // Name defines what is the name of the attribute. 147 | // This will be the raw Monolithe Specification name, without 148 | // Go translation. 149 | Name string 150 | 151 | // Orderable defines if it is possible to order based on the value of this attribute. 152 | // 153 | // This is not enforced by elemental. You must write your own business logic to honor this. 154 | Orderable bool 155 | 156 | // PrimaryKey defines if the attribute is used as a primary key. 157 | PrimaryKey bool 158 | 159 | // ReadOnly defines if the attribute is read only. 160 | // 161 | // This is not enforced by elemental. You must write your own business logic to honor this. 162 | ReadOnly bool 163 | 164 | // Required defines is the attribute must be set or not. 165 | // 166 | // This is enforced by elemental. 167 | Required bool 168 | 169 | // Secret defines if the attribute is secret. 170 | // This is useful if you can to define perform sanity check on this field to be sure it 171 | // is not sent for instance. 172 | Secret bool 173 | 174 | // Setter defines if the attribute needs to define a setter method. 175 | // This is useful if you can to define an Interface based on this attribute. 176 | Setter bool 177 | 178 | // Signed indicates if the attribute's value should be used when 179 | // generating a signature for the object. 180 | Signed bool 181 | 182 | // Stored defines if the attribute will be stored in the northbound API. 183 | // 184 | // If this is true, the backend tags will be generated by Monolithe. 185 | Stored bool 186 | 187 | // SubType defines the Monolithe Subtype. 188 | SubType string 189 | 190 | // Transient defines if the attributes is transient or not. 191 | // 192 | // This is not enforced by elemental. You must write your own business logic to honor this. 193 | Transient bool 194 | 195 | // Type defines the raw Monolithe type. 196 | Type string 197 | 198 | // Encrypted defines if the attribute needs encryption. 199 | Encrypted bool 200 | } 201 | 202 | // ResetSecretAttributesValues will reset any attributes marked 203 | // as `secret` in the given obj if it is an elemental.Identifiable 204 | // or an elemental.Identifiables. 205 | // The given Identifiables must implement the elemental.AttributeSpecifiable 206 | // interface or this function will have no effect. 207 | // 208 | // If you pass anything else, this function does nothing. 209 | func ResetSecretAttributesValues(obj any) { 210 | 211 | if obj == nil { 212 | return 213 | } 214 | 215 | v := reflect.ValueOf(obj) 216 | if v.Kind() == reflect.Ptr && v.IsNil() { 217 | return 218 | } 219 | 220 | strip := func(o Identifiable) { 221 | 222 | oo := o 223 | if sp, ok := o.(SparseIdentifiable); ok { 224 | oo = sp.ToPlain() 225 | } 226 | 227 | if attrspec, ok := oo.(AttributeSpecifiable); ok { 228 | 229 | var rv, val reflect.Value 230 | 231 | for _, aspec := range attrspec.AttributeSpecifications() { 232 | 233 | if !aspec.Secret { 234 | continue 235 | } 236 | 237 | rv = reflect.Indirect(reflect.ValueOf(o)) 238 | val = rv.FieldByName(aspec.ConvertedName) 239 | val.Set(reflect.Zero(val.Type())) 240 | } 241 | } 242 | } 243 | 244 | switch o := obj.(type) { 245 | 246 | case Identifiable: 247 | strip(o) 248 | 249 | case Identifiables: 250 | for _, i := range o.List() { 251 | strip(i) 252 | } 253 | } 254 | } 255 | 256 | // aesAttributeEncrypter is an elemental.AttributeEncrypter 257 | // using AES encryption. 258 | type aesAttributeEncrypter struct { 259 | passphrase []byte 260 | } 261 | 262 | // NewAESAttributeEncrypter returns a new elemental.AttributeEncrypter 263 | // implementing AES encryption. 264 | func NewAESAttributeEncrypter(passphrase string) (AttributeEncrypter, error) { 265 | 266 | passbytes := []byte(passphrase) 267 | if len(passbytes) != aes.BlockSize { 268 | return nil, fmt.Errorf("invalid passphrase: size must be exactly %d bytes", aes.BlockSize) 269 | } 270 | 271 | return &aesAttributeEncrypter{ 272 | passphrase: passbytes, 273 | }, nil 274 | } 275 | 276 | // EncryptString encrypts the given string. 277 | func (e *aesAttributeEncrypter) EncryptString(value string) (string, error) { 278 | 279 | if value == "" { 280 | return "", nil 281 | } 282 | 283 | data := []byte(value) 284 | 285 | c, err := aes.NewCipher(e.passphrase) 286 | if err != nil { 287 | return "", err 288 | } 289 | 290 | gcm, err := cipher.NewGCM(c) 291 | if err != nil { 292 | return "", err 293 | } 294 | 295 | nonce := make([]byte, gcm.NonceSize()) 296 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 297 | return "", err 298 | } 299 | 300 | return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, data, nil)), nil 301 | } 302 | 303 | // DecryptString decrypts the given string. 304 | func (e *aesAttributeEncrypter) DecryptString(value string) (string, error) { 305 | 306 | if value == "" { 307 | return "", nil 308 | } 309 | 310 | data, err := base64.StdEncoding.DecodeString(value) 311 | if err != nil { 312 | return "", err 313 | } 314 | 315 | c, err := aes.NewCipher(e.passphrase) 316 | if err != nil { 317 | return "", err 318 | } 319 | 320 | gcm, err := cipher.NewGCM(c) 321 | if err != nil { 322 | return "", err 323 | } 324 | 325 | nonceSize := gcm.NonceSize() 326 | if len(data) < nonceSize { 327 | return "", fmt.Errorf("data is too small") 328 | } 329 | 330 | out, err := gcm.Open(nil, data[:nonceSize], data[nonceSize:], nil) 331 | if err != nil { 332 | return "", err 333 | } 334 | 335 | return string(out), nil 336 | } 337 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "io" 18 | "mime" 19 | "net/http" 20 | "reflect" 21 | "strings" 22 | "sync" 23 | 24 | "github.com/ugorji/go/codec" 25 | ) 26 | 27 | var ( 28 | externalSupportedContentType = map[string]struct{}{} 29 | externalSupportedAcceptType = map[string]struct{}{} 30 | ) 31 | 32 | // RegisterSupportedContentType registers a new media type 33 | // that elemental should support for Content-Type. 34 | // Note that this needs external intervention to handle encoding. 35 | func RegisterSupportedContentType(mimetype string) { 36 | externalSupportedContentType[mimetype] = struct{}{} 37 | } 38 | 39 | // RegisterSupportedAcceptType registers a new media type 40 | // that elemental should support for Accept. 41 | // Note that this needs external intervention to handle decoding. 42 | func RegisterSupportedAcceptType(mimetype string) { 43 | externalSupportedAcceptType[mimetype] = struct{}{} 44 | } 45 | 46 | // An Encodable is the interface of objects 47 | // that can hold encoding information. 48 | type Encodable interface { 49 | GetEncoding() EncodingType 50 | } 51 | 52 | // A Encoder is an Encodable that can be encoded. 53 | type Encoder interface { 54 | Encode(obj any) (err error) 55 | Encodable 56 | } 57 | 58 | // A Decoder is an Encodable that can be decoded. 59 | type Decoder interface { 60 | Decode(dst any) error 61 | Encodable 62 | } 63 | 64 | // An EncodingType represents one type of data encoding 65 | type EncodingType string 66 | 67 | // Various values for EncodingType. 68 | const ( 69 | EncodingTypeJSON EncodingType = "application/json" 70 | EncodingTypeMSGPACK EncodingType = "application/msgpack" 71 | ) 72 | 73 | var ( 74 | jsonHandle = &codec.JsonHandle{} 75 | jsonEncodersPool = sync.Pool{ 76 | New: func() any { 77 | return codec.NewEncoder(nil, jsonHandle) 78 | }, 79 | } 80 | jsonDecodersPool = sync.Pool{ 81 | New: func() any { 82 | return codec.NewDecoder(nil, jsonHandle) 83 | }, 84 | } 85 | 86 | msgpackHandle = &codec.MsgpackHandle{} 87 | msgpackEncodersPool = sync.Pool{ 88 | New: func() any { 89 | return codec.NewEncoder(nil, msgpackHandle) 90 | }, 91 | } 92 | msgpackDecodersPool = sync.Pool{ 93 | New: func() any { 94 | return codec.NewDecoder(nil, msgpackHandle) 95 | }, 96 | } 97 | ) 98 | 99 | func init() { 100 | // If you need to understand all of this, go there http://ugorji.net/blog/go-codec-primer 101 | // But you should not need to touch that. 102 | jsonHandle.Canonical = true 103 | jsonHandle.MapType = reflect.TypeOf(map[string]any(nil)) 104 | 105 | msgpackHandle.Canonical = true 106 | msgpackHandle.WriteExt = true 107 | msgpackHandle.MapType = reflect.TypeOf(map[string]any(nil)) 108 | msgpackHandle.TypeInfos = codec.NewTypeInfos([]string{"msgpack"}) 109 | } 110 | 111 | // Decode decodes the given data using an appropriate decoder chosen 112 | // from the given encoding. 113 | func Decode(encoding EncodingType, data []byte, dest any) error { 114 | 115 | var pool *sync.Pool 116 | 117 | switch encoding { 118 | case EncodingTypeMSGPACK: 119 | pool = &msgpackDecodersPool 120 | default: 121 | pool = &jsonDecodersPool 122 | encoding = EncodingTypeJSON 123 | } 124 | 125 | res := pool.Get() 126 | dec, ok := res.(*codec.Decoder) 127 | if !ok { 128 | return fmt.Errorf("expected codec.Decoder from pool, got unexpected type: %T", res) 129 | } 130 | defer pool.Put(dec) 131 | 132 | dec.Reset(bytes.NewBuffer(data)) 133 | 134 | if err := dec.Decode(dest); err != nil { 135 | return fmt.Errorf("unable to decode %s: %s", encoding, err.Error()) 136 | } 137 | 138 | return nil 139 | } 140 | 141 | // Encode encodes the given object using an appropriate encoder chosen 142 | // from the given acceptType. 143 | func Encode(encoding EncodingType, obj any) ([]byte, error) { 144 | 145 | if obj == nil { 146 | return nil, fmt.Errorf("encode received a nil object") 147 | } 148 | 149 | var pool *sync.Pool 150 | 151 | switch encoding { 152 | case EncodingTypeMSGPACK: 153 | pool = &msgpackEncodersPool 154 | default: 155 | pool = &jsonEncodersPool 156 | encoding = EncodingTypeJSON 157 | } 158 | 159 | res := pool.Get() 160 | enc, ok := res.(*codec.Encoder) 161 | if !ok { 162 | return nil, fmt.Errorf("expected codec.Encoder from pool, got unexpected type: %T", res) 163 | } 164 | defer pool.Put(enc) 165 | 166 | buf := bytes.NewBuffer(nil) 167 | enc.Reset(buf) 168 | 169 | if err := enc.Encode(obj); err != nil { 170 | return nil, fmt.Errorf("unable to encode %s: %s", encoding, err.Error()) 171 | } 172 | 173 | return buf.Bytes(), nil 174 | } 175 | 176 | // MakeStreamDecoder returns a function that can be used to decode a stream from the 177 | // given reader using the given encoding. 178 | // 179 | // This function returns the decoder function that can be called until it returns an 180 | // io.EOF error, indicating the stream is over, and a dispose function that will 181 | // put back the decoder in the memory pool. 182 | // The dispose function will be called automatically when the decoding is over, 183 | // but not on a single decoding error. 184 | // In any case, the dispose function should be always called, in a defer for example. 185 | func MakeStreamDecoder(encoding EncodingType, reader io.Reader) (func(dest any) error, func()) { 186 | 187 | var pool *sync.Pool 188 | 189 | switch encoding { 190 | case EncodingTypeMSGPACK: 191 | pool = &msgpackDecodersPool 192 | default: 193 | pool = &jsonDecodersPool 194 | } 195 | 196 | res := pool.Get() 197 | dec, ok := res.(*codec.Decoder) 198 | if !ok { 199 | return func(dest any) error { 200 | return fmt.Errorf("expected codec.Decoder from pool, got unexpected type: %T", res) 201 | }, func() {} 202 | } 203 | dec.Reset(reader) 204 | 205 | clean := func() { 206 | if pool != nil { 207 | pool.Put(dec) 208 | pool = nil 209 | } 210 | } 211 | 212 | return func(dest any) error { 213 | 214 | if err := dec.Decode(dest); err != nil { 215 | 216 | if err == io.EOF { 217 | clean() 218 | return err 219 | } 220 | 221 | return fmt.Errorf("unable to decode %s: %s", encoding, err.Error()) 222 | } 223 | 224 | return nil 225 | }, func() { 226 | clean() 227 | } 228 | } 229 | 230 | // MakeStreamEncoder returns a function that can be user en encode given data 231 | // into the given io.Writer using the given encoding. 232 | // 233 | // It also returns a function must be called once the encoding procedure 234 | // is complete, so the internal encoders can be put back into the shared 235 | // memory pools. 236 | func MakeStreamEncoder(encoding EncodingType, writer io.Writer) (func(obj any) error, func()) { 237 | 238 | var pool *sync.Pool 239 | 240 | switch encoding { 241 | case EncodingTypeMSGPACK: 242 | pool = &msgpackEncodersPool 243 | default: 244 | pool = &jsonEncodersPool 245 | } 246 | 247 | res := pool.Get() 248 | enc, ok := res.(*codec.Encoder) 249 | if !ok { 250 | return func(dest any) error { 251 | return fmt.Errorf("expected codec.Encoder from pool, got unexpected type: %T", res) 252 | }, func() {} 253 | } 254 | enc.Reset(writer) 255 | 256 | clean := func() { 257 | if pool != nil { 258 | pool.Put(enc) 259 | pool = nil 260 | } 261 | } 262 | 263 | return func(dest any) error { 264 | 265 | if err := enc.Encode(dest); err != nil { 266 | return fmt.Errorf("unable to encode %s: %s", encoding, err.Error()) 267 | } 268 | 269 | return nil 270 | }, func() { 271 | clean() 272 | } 273 | } 274 | 275 | // Convert converts from one EncodingType to another 276 | func Convert(from EncodingType, to EncodingType, data []byte) ([]byte, error) { 277 | 278 | if from == to { 279 | return data, nil 280 | } 281 | 282 | m := map[string]any{} 283 | if err := Decode(from, data, &m); err != nil { 284 | return nil, err 285 | } 286 | 287 | return Encode(to, m) 288 | } 289 | 290 | // EncodingFromHeaders returns the read (Content-Type) and write (Accept) encoding 291 | // from the given http.Header. 292 | func EncodingFromHeaders(header http.Header) (read EncodingType, write EncodingType, err error) { 293 | 294 | read = EncodingTypeJSON 295 | write = EncodingTypeJSON 296 | 297 | if header == nil { 298 | return read, write, nil 299 | } 300 | 301 | if v := header.Get("Content-Type"); v != "" { 302 | ct, _, err := mime.ParseMediaType(v) 303 | if err != nil { 304 | return "", "", NewError("Bad Request", fmt.Sprintf("Invalid Content-Type header: %s", err), "elemental", http.StatusBadRequest) 305 | } 306 | 307 | switch ct { 308 | 309 | case "application/msgpack": 310 | read = EncodingTypeMSGPACK 311 | 312 | case "application/*", "*/*", "application/json": 313 | read = EncodingTypeJSON 314 | 315 | default: 316 | var supported bool 317 | for t := range externalSupportedContentType { 318 | if ct == t { 319 | supported = true 320 | break 321 | } 322 | } 323 | if !supported { 324 | return "", "", NewError("Unsupported Media Type", fmt.Sprintf("Cannot find any acceptable Content-Type media type in provided header: %s", v), "elemental", http.StatusUnsupportedMediaType) 325 | } 326 | 327 | read = EncodingType(ct) 328 | } 329 | } 330 | 331 | if v := header.Get("Accept"); v != "" { 332 | var agreed bool 333 | L: 334 | for _, item := range strings.Split(v, ",") { 335 | 336 | at, _, err := mime.ParseMediaType(item) 337 | if err != nil { 338 | return "", "", NewError("Bad Request", fmt.Sprintf("Invalid Accept header: %s", err), "elemental", http.StatusBadRequest) 339 | } 340 | 341 | switch at { 342 | 343 | case "application/msgpack": 344 | write = EncodingTypeMSGPACK 345 | agreed = true 346 | break L 347 | 348 | case "application/*", "*/*", "application/json": 349 | write = EncodingTypeJSON 350 | agreed = true 351 | break L 352 | 353 | default: 354 | for t := range externalSupportedAcceptType { 355 | if at == t { 356 | agreed = true 357 | write = EncodingType(at) 358 | break L 359 | } 360 | } 361 | } 362 | } 363 | 364 | if !agreed { 365 | return "", "", NewError("Unsupported Media Type", fmt.Sprintf("Cannot find any acceptable Accept media type in provided header: %s", v), "elemental", http.StatusUnsupportedMediaType) 366 | } 367 | } 368 | 369 | return read, write, nil 370 | } 371 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Aporeto Inc. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package elemental 13 | 14 | import ( 15 | "encoding/json" 16 | "fmt" 17 | "testing" 18 | 19 | . "github.com/smartystreets/goconvey/convey" 20 | ) 21 | 22 | func TestEvent_NewEvent(t *testing.T) { 23 | 24 | Convey("Given I create an Event using EncodingTypeJSON", t, func() { 25 | 26 | list := &List{} 27 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 28 | 29 | Convey("Then the Event should be correctly initialized", func() { 30 | d, _ := Encode(EncodingTypeJSON, list) 31 | So(e.Identity, ShouldEqual, "list") 32 | So(e.Type, ShouldEqual, EventCreate) 33 | So(e.Encoding, ShouldEqual, EncodingTypeJSON) 34 | So(e.JSONData, ShouldResemble, json.RawMessage(d)) 35 | So(e.RawData, ShouldBeNil) 36 | So(e.Entity(), ShouldResemble, []byte(e.JSONData)) 37 | }) 38 | }) 39 | 40 | Convey("Given I create an Event using EncodingTypeMSGPACK", t, func() { 41 | 42 | list := &List{} 43 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeMSGPACK) 44 | 45 | Convey("Then the Event should be correctly initialized", func() { 46 | d, _ := Encode(EncodingTypeMSGPACK, list) 47 | So(e.Identity, ShouldEqual, "list") 48 | So(e.Type, ShouldEqual, EventCreate) 49 | So(e.Encoding, ShouldEqual, EncodingTypeMSGPACK) 50 | So(e.JSONData, ShouldBeNil) 51 | So(e.RawData, ShouldResemble, d) 52 | So(e.Entity(), ShouldResemble, e.RawData) 53 | }) 54 | }) 55 | 56 | Convey("Given I create an Event with an unmarshalable entity", t, func() { 57 | 58 | Convey("Then it should panic", func() { 59 | So(func() { NewEvent(EventCreate, nil) }, ShouldPanicWith, "unable to create new event: encode received a nil object") 60 | }) 61 | }) 62 | } 63 | 64 | func TestNewErrorEvent(t *testing.T) { 65 | 66 | testErr := Error{ 67 | Description: "some description", 68 | Subject: "some subject", 69 | Title: "some title", 70 | Data: map[string]any{ 71 | "someKey": "someValue", 72 | }, 73 | } 74 | 75 | Convey("Given I create an error Event using EncodingTypeJSON", t, func() { 76 | 77 | e := NewErrorEvent(testErr, EncodingTypeJSON) 78 | 79 | Convey("Then the error Event should be correctly initialized", func() { 80 | d, _ := Encode(EncodingTypeJSON, testErr) 81 | So(e.Identity, ShouldEqual, "") 82 | So(e.Type, ShouldEqual, EventError) 83 | So(e.Encoding, ShouldEqual, EncodingTypeJSON) 84 | So(e.JSONData, ShouldResemble, json.RawMessage(d)) 85 | So(e.RawData, ShouldBeNil) 86 | So(e.Entity(), ShouldResemble, []byte(e.JSONData)) 87 | }) 88 | }) 89 | 90 | Convey("Given I create an error Event using EncodingTypeMSGPACK", t, func() { 91 | 92 | e := NewErrorEvent(testErr, EncodingTypeMSGPACK) 93 | 94 | Convey("Then the error Event should be correctly initialized", func() { 95 | d, _ := Encode(EncodingTypeMSGPACK, testErr) 96 | So(e.Identity, ShouldEqual, "") 97 | So(e.Type, ShouldEqual, EventError) 98 | So(e.Encoding, ShouldEqual, EncodingTypeMSGPACK) 99 | So(e.JSONData, ShouldBeNil) 100 | So(e.RawData, ShouldResemble, d) 101 | So(e.Entity(), ShouldResemble, e.RawData) 102 | }) 103 | }) 104 | } 105 | 106 | func TestEvent_Decode(t *testing.T) { 107 | 108 | Convey("Given I create an Event using EncodingTypeJSON", t, func() { 109 | 110 | list := &List{Name: "t1"} 111 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 112 | 113 | Convey("When I decode the data", func() { 114 | l2 := &List{} 115 | 116 | _ = e.Decode(l2) 117 | 118 | Convey("Then t2 should resemble to tag", func() { 119 | So(l2, ShouldResemble, list) 120 | }) 121 | }) 122 | }) 123 | 124 | Convey("Given I create an Event using EncodingTypeMSGPACK", t, func() { 125 | 126 | list := &List{Name: "t1"} 127 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeMSGPACK) 128 | 129 | Convey("When I decode the data", func() { 130 | l2 := &List{} 131 | 132 | _ = e.Decode(l2) 133 | 134 | Convey("Then t2 should resemble to tag", func() { 135 | So(l2, ShouldResemble, list) 136 | }) 137 | }) 138 | }) 139 | } 140 | 141 | func TestEvent_String(t *testing.T) { 142 | 143 | Convey("Given I create an Event", t, func() { 144 | 145 | list := &List{Name: "t1"} 146 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 147 | 148 | Convey("When I use String", func() { 149 | str := e.String() 150 | 151 | Convey("Then the string representatipn should be correct", func() { 152 | So(str, ShouldEqual, "") 153 | }) 154 | }) 155 | }) 156 | } 157 | 158 | func TestEvent_NewEvents(t *testing.T) { 159 | 160 | Convey("Given I create an Events", t, func() { 161 | 162 | list := &List{} 163 | e1 := NewEvent(EventCreate, list) 164 | e2 := NewEvent(EventDelete, list) 165 | 166 | evts := NewEvents(e1, e2) 167 | 168 | Convey("Then the Event should be correctly initialized", func() { 169 | So(len(evts), ShouldEqual, 2) 170 | }) 171 | }) 172 | } 173 | 174 | func TestEvent_Convert(t *testing.T) { 175 | 176 | Convey("Given I have an Event with EncodingTypeJSON data", t, func() { 177 | 178 | list := &List{ 179 | Name: "hello", 180 | } 181 | 182 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 183 | 184 | Convey("When I Convert to EncodingTypeMSGPACK", func() { 185 | 186 | err := e.Convert(EncodingTypeMSGPACK) 187 | 188 | Convey("Then err should be nil", func() { 189 | So(err, ShouldBeNil) 190 | }) 191 | 192 | Convey("Then the converted event should be correct", func() { 193 | // Here there is some changes in the way time is econded, making the bytes not completely equals 194 | l2 := &List{} 195 | _ = Decode(EncodingTypeMSGPACK, e.Entity(), l2) 196 | So(e.JSONData, ShouldBeNil) 197 | So(e.Encoding, ShouldEqual, EncodingTypeMSGPACK) 198 | So(list, ShouldResemble, l2) 199 | }) 200 | }) 201 | }) 202 | 203 | Convey("Given I have an Event with EncodingTypeMSGPACK data", t, func() { 204 | 205 | list := &List{ 206 | Name: "hello", 207 | } 208 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeMSGPACK) 209 | 210 | Convey("When I Convert to EncodingTypeJSON", func() { 211 | 212 | err := e.Convert(EncodingTypeJSON) 213 | 214 | Convey("Then err should be nil", func() { 215 | So(err, ShouldBeNil) 216 | }) 217 | 218 | Convey("Then the converted event should be correct", func() { 219 | d, _ := Encode(EncodingTypeJSON, list) 220 | So(string(e.Entity()), ShouldResemble, string(d)) 221 | So(e.JSONData, ShouldResemble, json.RawMessage(d)) 222 | So(e.RawData, ShouldBeNil) 223 | So(e.Encoding, ShouldEqual, EncodingTypeJSON) 224 | }) 225 | }) 226 | 227 | Convey("When I Convert to EncodingTypeMSGPACK", func() { 228 | 229 | err := e.Convert(EncodingTypeMSGPACK) 230 | 231 | Convey("Then err should be nil", func() { 232 | So(err, ShouldBeNil) 233 | }) 234 | 235 | Convey("Then the converted event should be correct", func() { 236 | d, _ := Encode(EncodingTypeMSGPACK, list) 237 | So(string(e.Entity()), ShouldResemble, string(d)) 238 | So(e.JSONData, ShouldBeNil) 239 | So(e.RawData, ShouldResemble, d) 240 | So(e.Encoding, ShouldEqual, EncodingTypeMSGPACK) 241 | }) 242 | }) 243 | }) 244 | 245 | Convey("Given I have an Event with invalid msgpack data", t, func() { 246 | 247 | list := &List{ 248 | Name: "hello", 249 | } 250 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeMSGPACK) 251 | e.RawData = []byte("not-good") 252 | 253 | Convey("When I Convert to EncodingTypeMSGPACK", func() { 254 | 255 | err := e.Convert(EncodingTypeJSON) 256 | 257 | Convey("Then err should be nil", func() { 258 | So(err, ShouldNotBeNil) 259 | }) 260 | 261 | Convey("Then the event should still be correct", func() { 262 | So(e.JSONData, ShouldBeNil) 263 | So(e.RawData, ShouldNotBeNil) 264 | So(e.Encoding, ShouldEqual, EncodingTypeMSGPACK) 265 | }) 266 | }) 267 | }) 268 | 269 | Convey("Given I have an Event with invalid json data", t, func() { 270 | 271 | list := &List{ 272 | Name: "hello", 273 | } 274 | e := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 275 | e.JSONData = []byte("not-good") 276 | 277 | Convey("When I Convert to EncodingTypeMSGPACK", func() { 278 | 279 | err := e.Convert(EncodingTypeMSGPACK) 280 | 281 | Convey("Then err should be nil", func() { 282 | So(err, ShouldNotBeNil) 283 | }) 284 | 285 | Convey("Then the event should still be correct", func() { 286 | So(e.JSONData, ShouldNotBeNil) 287 | So(e.RawData, ShouldBeNil) 288 | So(e.Encoding, ShouldEqual, EncodingTypeJSON) 289 | }) 290 | }) 291 | }) 292 | } 293 | 294 | func TestEvent_Duplicate(t *testing.T) { 295 | 296 | Convey("Given I have an Event with encoding set to json", t, func() { 297 | 298 | list := &List{} 299 | e1 := NewEventWithEncoding(EventCreate, list, EncodingTypeJSON) 300 | 301 | Convey("When I Duplicate ", func() { 302 | 303 | e2 := e1.Duplicate() 304 | 305 | Convey("Then the duplicated event should be correct", func() { 306 | So(e2.Type, ShouldEqual, e1.Type) 307 | So(e2.Entity(), ShouldResemble, e1.Entity()) 308 | So(e2.RawData, ShouldBeNil) 309 | So(e2.JSONData, ShouldResemble, e1.JSONData) 310 | So(fmt.Sprintf("%p", e2.JSONData), ShouldNotEqual, fmt.Sprintf("%p", e1.JSONData)) 311 | So(e2.Identity, ShouldEqual, e1.Identity) 312 | So(e2.Timestamp, ShouldEqual, e1.Timestamp) 313 | So(e2.Encoding, ShouldEqual, e1.Encoding) 314 | }) 315 | }) 316 | }) 317 | 318 | Convey("Given I have an Event with encoding set to msgpack", t, func() { 319 | 320 | list := &List{} 321 | e1 := NewEventWithEncoding(EventCreate, list, EncodingTypeMSGPACK) 322 | 323 | Convey("When I Duplicate ", func() { 324 | 325 | e2 := e1.Duplicate() 326 | 327 | Convey("Then the duplicated event should be correct", func() { 328 | So(e2.Type, ShouldEqual, e1.Type) 329 | So(e2.Entity(), ShouldResemble, e1.Entity()) 330 | So(e2.RawData, ShouldResemble, e1.RawData) 331 | So(fmt.Sprintf("%p", e2.RawData), ShouldNotEqual, fmt.Sprintf("%p", e1.RawData)) 332 | So(e2.JSONData, ShouldBeNil) 333 | So(e2.Identity, ShouldEqual, e1.Identity) 334 | So(e2.Timestamp, ShouldEqual, e1.Timestamp) 335 | So(e2.Encoding, ShouldEqual, e1.Encoding) 336 | }) 337 | }) 338 | }) 339 | } 340 | -------------------------------------------------------------------------------- /cmd/internal/genopenapi3/converter_relations_spec_root_test.go: -------------------------------------------------------------------------------- 1 | package genopenapi3 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConverter_Do__specRelations_root(t *testing.T) { 8 | t.Parallel() 9 | 10 | cases := map[string]testCase{ 11 | 12 | "relation-create": { 13 | inSpec: ` 14 | model: 15 | root: true 16 | rest_name: root 17 | resource_name: root 18 | entity_name: Root 19 | package: root 20 | group: core 21 | description: root object. 22 | 23 | relations: 24 | - rest_name: resource 25 | create: 26 | description: Creates some resource. 27 | parameters: 28 | entries: 29 | - name: fancyParam 30 | description: This is a fancy parameter. 31 | type: integer 32 | `, 33 | outDocs: map[string]string{ 34 | "toplevel": ` 35 | { 36 | "openapi": "3.0.3", 37 | "tags":[ 38 | { 39 | "name": "useful/thing", 40 | "description": "This tag is for group 'useful/thing'" 41 | }, 42 | { 43 | "name": "usefulPackageName", 44 | "description": "This tag is for package 'usefulPackageName'" 45 | } 46 | ], 47 | "info": { 48 | "contact": { 49 | "email": "dev@aporeto.com", 50 | "name": "Aporeto Inc.", 51 | "url": "go.aporeto.io/api" 52 | }, 53 | "license": { 54 | "name": "TODO" 55 | }, 56 | "termsOfService": "https://localhost/TODO", 57 | "version": "1.0", 58 | "title": "toplevel" 59 | }, 60 | "components": { 61 | "schemas": { 62 | "resource": { 63 | "description": "Represents a resource.", 64 | "type": "object" 65 | } 66 | } 67 | }, 68 | "paths": { 69 | "/resources": { 70 | "post": { 71 | "operationId": "create-a-new-resource", 72 | "tags": ["useful/thing", "usefulPackageName"], 73 | "parameters": [ 74 | { 75 | "description": "This is a fancy parameter.", 76 | "in": "query", 77 | "name": "fancyParam", 78 | "schema": { 79 | "type": "integer" 80 | } 81 | } 82 | ], 83 | "description": "Creates some resource.", 84 | "requestBody": { 85 | "content": { 86 | "application/json": { 87 | "schema": { 88 | "$ref": "#/components/schemas/resource" 89 | } 90 | } 91 | } 92 | }, 93 | "responses": { 94 | "200": { 95 | "description": "n/a", 96 | "content": { 97 | "application/json": { 98 | "schema": { 99 | "$ref": "#/components/schemas/resource" 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | `, 110 | }, 111 | supportingSpecs: []string{` 112 | model: 113 | rest_name: resource 114 | resource_name: resources 115 | entity_name: Recource 116 | package: usefulPackageName 117 | group: useful/thing 118 | description: Represents a resource. 119 | `}, 120 | }, 121 | 122 | "relation-get": { 123 | inSpec: ` 124 | model: 125 | root: true 126 | rest_name: root 127 | resource_name: root 128 | entity_name: Root 129 | package: root 130 | group: core 131 | description: root object. 132 | 133 | relations: 134 | - rest_name: resource 135 | get: 136 | description: Retrieve all resources. 137 | parameters: 138 | entries: 139 | - name: fancyParam 140 | description: This is a fancy parameter. 141 | type: boolean 142 | `, 143 | outDocs: map[string]string{ 144 | "toplevel": ` 145 | { 146 | "openapi": "3.0.3", 147 | "tags": [ 148 | { 149 | "name": "useful/thing", 150 | "description": "This tag is for group 'useful/thing'" 151 | }, 152 | { 153 | "name": "usefulPackageName", 154 | "description": "This tag is for package 'usefulPackageName'" 155 | } 156 | ], 157 | "info": { 158 | "contact": { 159 | "email": "dev@aporeto.com", 160 | "name": "Aporeto Inc.", 161 | "url": "go.aporeto.io/api" 162 | }, 163 | "license": { 164 | "name": "TODO" 165 | }, 166 | "termsOfService": "https://localhost/TODO", 167 | "version": "1.0", 168 | "title": "toplevel" 169 | }, 170 | "components": { 171 | "schemas": { 172 | "resource": { 173 | "description": "Represents a resource.", 174 | "type": "object" 175 | } 176 | } 177 | }, 178 | "paths": { 179 | "/resources": { 180 | "get": { 181 | "operationId": "get-all-resources", 182 | "tags": ["useful/thing", "usefulPackageName"], 183 | "description": "Retrieve all resources.", 184 | "parameters": [ 185 | { 186 | "description": "This is a fancy parameter.", 187 | "in": "query", 188 | "name": "fancyParam", 189 | "schema": { 190 | "type": "boolean" 191 | } 192 | } 193 | ], 194 | "responses": { 195 | "200": { 196 | "description": "n/a", 197 | "content": { 198 | "application/json": { 199 | "schema": { 200 | "type": "array", 201 | "items": { 202 | "$ref": "#/components/schemas/resource" 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | `, 214 | }, 215 | supportingSpecs: []string{` 216 | model: 217 | rest_name: resource 218 | resource_name: resources 219 | entity_name: Recource 220 | package: usefulPackageName 221 | group: useful/thing 222 | description: Represents a resource. 223 | `}, 224 | }, 225 | 226 | "relation-without-get-or-create": { 227 | inSpec: ` 228 | model: 229 | root: true 230 | rest_name: root 231 | resource_name: root 232 | entity_name: Root 233 | package: root 234 | group: core 235 | description: root object. 236 | 237 | relations: 238 | - rest_name: resource 239 | `, 240 | outDocs: map[string]string{ 241 | "toplevel": ` 242 | { 243 | "openapi": "3.0.3", 244 | "tags":[ 245 | { 246 | "name": "useful/thing", 247 | "description": "This tag is for group 'useful/thing'" 248 | }, 249 | { 250 | "name": "usefulPackageName", 251 | "description": "This tag is for package 'usefulPackageName'" 252 | } 253 | ], 254 | "info": { 255 | "contact": { 256 | "email": "dev@aporeto.com", 257 | "name": "Aporeto Inc.", 258 | "url": "go.aporeto.io/api" 259 | }, 260 | "license": { 261 | "name": "TODO" 262 | }, 263 | "termsOfService": "https://localhost/TODO", 264 | "version": "1.0", 265 | "title": "toplevel" 266 | }, 267 | "components": { 268 | "schemas": { 269 | "resource": { 270 | "description": "Represents a resource.", 271 | "type": "object" 272 | } 273 | } 274 | }, 275 | "paths": {} 276 | } 277 | `, 278 | }, 279 | supportingSpecs: []string{` 280 | model: 281 | rest_name: resource 282 | resource_name: resources 283 | entity_name: Recource 284 | package: usefulPackageName 285 | group: useful/thing 286 | description: Represents a resource. 287 | `}, 288 | }, 289 | } 290 | runAllTestCases(t, cases) 291 | } 292 | 293 | func TestConverter_Do__specRelations_root_withPrivateModel(t *testing.T) { 294 | t.Parallel() 295 | 296 | inSpec := ` 297 | model: 298 | root: true 299 | rest_name: root 300 | resource_name: root 301 | entity_name: Root 302 | package: root 303 | group: core 304 | description: root object. 305 | 306 | relations: 307 | - rest_name: resource 308 | create: 309 | description: Creates some resource. 310 | - rest_name: hidden 311 | create: 312 | description: Creates some hidden secrets. 313 | ` 314 | 315 | outDoc := map[string]string{ 316 | "toplevel": ` 317 | { 318 | "openapi": "3.0.3", 319 | "tags":[ 320 | { 321 | "name": "useful/thing", 322 | "description": "This tag is for group 'useful/thing'" 323 | }, 324 | { 325 | "name": "usefulPackageName", 326 | "description": "This tag is for package 'usefulPackageName'" 327 | } 328 | ], 329 | "info": { 330 | "contact": { 331 | "email": "dev@aporeto.com", 332 | "name": "Aporeto Inc.", 333 | "url": "go.aporeto.io/api" 334 | }, 335 | "license": { 336 | "name": "TODO" 337 | }, 338 | "termsOfService": "https://localhost/TODO", 339 | "version": "1.0", 340 | "title": "toplevel" 341 | }, 342 | "components": { 343 | "schemas": { 344 | "resource": { 345 | "description": "Represents a resource.", 346 | "type": "object" 347 | } 348 | } 349 | }, 350 | "paths": { 351 | "/resources": { 352 | "post": { 353 | "operationId": "create-a-new-resource", 354 | "tags": ["useful/thing", "usefulPackageName"], 355 | "description": "Creates some resource.", 356 | "requestBody": { 357 | "content": { 358 | "application/json": { 359 | "schema": { 360 | "$ref": "#/components/schemas/resource" 361 | } 362 | } 363 | } 364 | }, 365 | "responses": { 366 | "200": { 367 | "description": "n/a", 368 | "content": { 369 | "application/json": { 370 | "schema": { 371 | "$ref": "#/components/schemas/resource" 372 | } 373 | } 374 | } 375 | } 376 | } 377 | } 378 | } 379 | } 380 | } 381 | `, 382 | } 383 | 384 | supportingSpecs := []string{ 385 | ` 386 | model: 387 | rest_name: resource 388 | resource_name: resources 389 | entity_name: Recource 390 | package: usefulPackageName 391 | group: useful/thing 392 | description: Represents a resource. 393 | `, 394 | ` 395 | model: 396 | rest_name: hidden 397 | resource_name: hiddens 398 | entity_name: Hidden 399 | package: secrets 400 | group: gossip/talk 401 | description: Represents a hidden secret. 402 | private: true 403 | `, 404 | } 405 | 406 | testCaseWrapper := map[string]testCase{ 407 | "root-relation-has-private-model": { 408 | inSkipPrivateModels: true, 409 | inSpec: inSpec, 410 | supportingSpecs: supportingSpecs, 411 | outDocs: outDoc, 412 | }, 413 | } 414 | runAllTestCases(t, testCaseWrapper) 415 | } 416 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------