From 5a9298452828f61e46f76cb955681b851e19620a Mon Sep 17 00:00:00 2001 From: Maxim Panfilov Date: Tue, 24 Sep 2024 12:28:58 +0300 Subject: [PATCH 1/2] up to go-stacktrace 0.2.0, move fixtures to /fixtures --- {tests => fixtures}/common.raml | 0 {tests => fixtures}/dtype.json | 0 {tests => fixtures}/dtype.raml | 0 {tests => fixtures}/example.yaml | 0 {tests => fixtures}/library.raml | 0 {tests => fixtures}/nested_libs/common.raml | 0 .../nested_libs/sublibrary.raml | 0 {tests => fixtures}/other_lib.raml | 0 go.mod | 2 +- go.sum | 4 ++-- main_test.go => parse_test.go | 22 +++++-------------- 11 files changed, 8 insertions(+), 20 deletions(-) rename {tests => fixtures}/common.raml (100%) rename {tests => fixtures}/dtype.json (100%) rename {tests => fixtures}/dtype.raml (100%) rename {tests => fixtures}/example.yaml (100%) rename {tests => fixtures}/library.raml (100%) rename {tests => fixtures}/nested_libs/common.raml (100%) rename {tests => fixtures}/nested_libs/sublibrary.raml (100%) rename {tests => fixtures}/other_lib.raml (100%) rename main_test.go => parse_test.go (69%) diff --git a/tests/common.raml b/fixtures/common.raml similarity index 100% rename from tests/common.raml rename to fixtures/common.raml diff --git a/tests/dtype.json b/fixtures/dtype.json similarity index 100% rename from tests/dtype.json rename to fixtures/dtype.json diff --git a/tests/dtype.raml b/fixtures/dtype.raml similarity index 100% rename from tests/dtype.raml rename to fixtures/dtype.raml diff --git a/tests/example.yaml b/fixtures/example.yaml similarity index 100% rename from tests/example.yaml rename to fixtures/example.yaml diff --git a/tests/library.raml b/fixtures/library.raml similarity index 100% rename from tests/library.raml rename to fixtures/library.raml diff --git a/tests/nested_libs/common.raml b/fixtures/nested_libs/common.raml similarity index 100% rename from tests/nested_libs/common.raml rename to fixtures/nested_libs/common.raml diff --git a/tests/nested_libs/sublibrary.raml b/fixtures/nested_libs/sublibrary.raml similarity index 100% rename from tests/nested_libs/sublibrary.raml rename to fixtures/nested_libs/sublibrary.raml diff --git a/tests/other_lib.raml b/fixtures/other_lib.raml similarity index 100% rename from tests/other_lib.raml rename to fixtures/other_lib.raml diff --git a/go.mod b/go.mod index 5c2b1c7..096a413 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.6 toolchain go1.23.0 require ( - github.com/acronis/go-stacktrace v0.1.0 + github.com/acronis/go-stacktrace v0.2.0 github.com/antlr4-go/antlr/v4 v4.13.1 github.com/stretchr/testify v1.9.0 github.com/wk8/go-ordered-map/v2 v2.1.8 diff --git a/go.sum b/go.sum index 41539f8..311d6a5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/acronis/go-stacktrace v0.1.0 h1:XtE2lmIOzD7sFYqct5Bo+YmjQ7RhvtazfETsMsETYvo= -github.com/acronis/go-stacktrace v0.1.0/go.mod h1:FOvjPOpMOpJhNgt2adD+FEnOpzcOzUBeiRkPaAd2aLQ= +github.com/acronis/go-stacktrace v0.2.0 h1:aUME2BnO2WwBpmidhSq+C2cCm6T0i7u1mwraetKPyjQ= +github.com/acronis/go-stacktrace v0.2.0/go.mod h1:FOvjPOpMOpJhNgt2adD+FEnOpzcOzUBeiRkPaAd2aLQ= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= diff --git a/main_test.go b/parse_test.go similarity index 69% rename from main_test.go rename to parse_test.go index c912158..6c2ffc0 100644 --- a/main_test.go +++ b/parse_test.go @@ -1,30 +1,21 @@ package raml import ( - "fmt" + "log/slog" "runtime" "testing" "time" "github.com/stretchr/testify/require" - - "github.com/acronis/go-stacktrace" ) -func Test_main(t *testing.T) { +func Test_ParseFromPath(t *testing.T) { start := time.Now() - rml, err := ParseFromPath(`./tests/library.raml`, OptWithUnwrap(), OptWithValidate()) - if vErr, ok := stacktrace.Unwrap(err); ok { - t.Logf("ParseFromPath error:\n%s", vErr.Sprint(stacktrace.WithEnsureDuplicates())) - err = vErr - } + rml, err := ParseFromPath(`./fixtures/library.raml`, OptWithUnwrap(), OptWithValidate()) require.NoError(t, err) elapsed := time.Since(start) - t.Logf("ParseFromPath took %d ms\n", elapsed.Milliseconds()) - fmt.Printf("Library location: %s\n", rml.entryPoint.GetLocation()) - shapesAll := rml.GetShapes() - fmt.Printf("Total shapes: %d\n", len(shapesAll)) + slog.Info("ParseFromPath", "took ms", elapsed.Milliseconds(), "location", rml.entryPoint.GetLocation(), "total shapes", len(shapesAll)) conv := NewJSONSchemaConverter() for _, frag := range rml.fragmentsCache { @@ -70,8 +61,5 @@ func Test_main(t *testing.T) { func printMemUsage(t *testing.T) { var m runtime.MemStats runtime.ReadMemStats(&m) - t.Logf("Alloc = %v MiB", m.Alloc/1024/1024) - t.Logf("\tTotalAlloc = %v MiB", m.TotalAlloc/1024/1024) - t.Logf("\tSys = %v MiB", m.Sys/1024/1024) - t.Logf("\tNumGC = %v\n", m.NumGC) + slog.Info("Memory usage", "alloc MiB", m.Alloc/1024/1024, "total alloc MiB", m.TotalAlloc/1024/1024, "sys MiB", m.Sys/1024/1024, "num GC", m.NumGC) } From dcea91e7b6b2065de9630583ed60b9b41cfc117e Mon Sep 17 00:00:00 2001 From: Maxim Panfilov Date: Tue, 24 Sep 2024 12:29:36 +0300 Subject: [PATCH 2/2] add CLI for RAML --- .gitignore | 1 + Makefile | 23 ++- README.md | 452 +++++++++++++++++++++++++++++-------------- cmd/raml/go.mod | 26 +++ cmd/raml/go.sum | 45 +++++ cmd/raml/raml.go | 110 +++++++++++ cmd/raml/slog.go | 37 ++++ cmd/raml/validate.go | 47 +++++ 8 files changed, 593 insertions(+), 148 deletions(-) create mode 100644 cmd/raml/go.mod create mode 100644 cmd/raml/go.sum create mode 100644 cmd/raml/raml.go create mode 100644 cmd/raml/slog.go create mode 100644 cmd/raml/validate.go diff --git a/.gitignore b/.gitignore index 4a29c72..b591f30 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.tokens cover.html cover.out +.build \ No newline at end of file diff --git a/Makefile b/Makefile index 7ab5b15..c5af77d 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,6 @@ # Directory containing the Makefile. -PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) - export PATH := $(GOBIN):$(PATH) -BENCH_FLAGS ?= -cpuprofile=cpu.pprof -memprofile=mem.pprof -benchmem - -# Directories that we want to test and track coverage for. -TEST_DIRS = . - .PHONY: all all: lint cover @@ -23,3 +16,19 @@ test: cover: @go test -coverprofile=cover.out -coverpkg=./... ./... \ && go tool cover -html=cover.out -o cover.html + +.PHONY: build +build: go-build + +.PHONY: go-build +go-build: + @cd cmd/raml && go build -o ../../.build/raml + +.PHONY: install +install: go-install + +.PHONY: go-install +go-install: + @cd cmd/raml && \ + go install -v ./... \ + && echo `go list -f '{{.Module.Path}}'` has been installed to `go list -f '{{.Target}}'` && true diff --git a/README.md b/README.md index 5f922d7..25b4b5a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # RAML 1.0 parser for Go > [!WARNING] -> The parser is in active development. See supported features in the **Supported features of RAML 1.0 specification** section. +> The parser is in active development. See supported features in the **Supported features of RAML 1.0 specification** +> section. -This is an implementation of RAML 1.0 parser for Go according to [the official RAML 1.0 specification](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/). +This is an implementation of RAML 1.0 parser for Go according +to [the official RAML 1.0 specification](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/). This package aims to achieve the following: @@ -23,58 +25,61 @@ The following sections are currently implemented. See notes for each point: - [ ] RAML API definitions - [x] RAML Data Types - - [x] Defining Types - - [x] Type Declarations - - [x] Built-in Types - - [x] The "Any" Type - - [x] Object Type - - [x] Property Declarations (explicit and pattern properties) - - [x] Additional Properties - - [x] Object Type Specialization - - [x] Using Discriminator - - [x] Array Type - - [x] Scalar Types - - [x] String - - [x] Number - - [x] Integer - - [x] Boolean - - [x] Date - - [x] File - - [x] Nil Type - - [x] Union Type (mostly supported, lacks enum support) - - [x] JSON Schema types (supported, but validation is not implemented) - - [x] Recursive types - - [x] User-defined Facets - - [x] Determine Default Types - - [x] Type Expressions - - [x] Inheritance - - [ ] Multiple Inheritance (supported, but not fully compliant) - - [x] Inline Type Declarations - - [x] Defining Examples in RAML - - [x] Multiple Examples - - [x] Single Example - - [x] Validation against defined data type + - [x] Defining Types + - [x] Type Declarations + - [x] Built-in Types + - [x] The "Any" Type + - [x] Object Type + - [x] Property Declarations (explicit and pattern properties) + - [x] Additional Properties + - [x] Object Type Specialization + - [x] Using Discriminator + - [x] Array Type + - [x] Scalar Types + - [x] String + - [x] Number + - [x] Integer + - [x] Boolean + - [x] Date + - [x] File + - [x] Nil Type + - [x] Union Type (mostly supported, lacks enum support) + - [x] JSON Schema types (supported, but validation is not implemented) + - [x] Recursive types + - [x] User-defined Facets + - [x] Determine Default Types + - [x] Type Expressions + - [x] Inheritance + - [ ] Multiple Inheritance (supported, but not fully compliant) + - [x] Inline Type Declarations + - [x] Defining Examples in RAML + - [x] Multiple Examples + - [x] Single Example + - [x] Validation against defined data type - [ ] Annotations - - [x] Declaring Annotation Types - - [ ] Applying Annotations - - [ ] Annotating Scalar-valued Nodes - - [ ] Annotation Targets - - [x] Annotating types + - [x] Declaring Annotation Types + - [ ] Applying Annotations + - [ ] Annotating Scalar-valued Nodes + - [ ] Annotation Targets + - [x] Annotating types - [ ] Modularization - - [ ] Includes - - [x] Library - - [x] NamedExample - - [x] DataType - - [ ] AnnotationTypeDeclaration - - [ ] DocumentationItem - - [ ] ResourceType - - [ ] Trait - - [ ] Overlay - - [ ] Extension - - [ ] SecurityScheme + - [ ] Includes + - [x] Library + - [x] NamedExample + - [x] DataType + - [ ] AnnotationTypeDeclaration + - [ ] DocumentationItem + - [ ] ResourceType + - [ ] Trait + - [ ] Overlay + - [ ] Extension + - [ ] SecurityScheme - [ ] Conversion - - [x] Conversion to JSON Schema - - [ ] Conversion to RAML + - [x] Conversion to JSON Schema + - [ ] Conversion to RAML +- [ ] CLI + - [x] Validate + - [ ] Convert to JSON Schema ## Comparison to existing libraries @@ -91,61 +96,82 @@ The following sections are currently implemented. See notes for each point: Complex project (7124 types, 148 libraries) -| | AML Modeling Framework (TS) | go-raml | -|------------|-----------------------------|---------| -| Time taken | ~17s | ~280ms | -| RAM taken | ~870MB | ~48MB | +| Project Type | Time taken | RAM taken | +|-----------------------------|------------|-----------| +| go-raml | ~280ms | ~48MB | +| AML Modeling Framework (TS) | ~17s | ~870MB | Simple project (<100 types, 1 library) -| | AML Modeling Framework (TS) | go-raml | -|------------|-----------------------------|---------| -| Time taken | ~2s | ~4ms | -| RAM taken | ~100MB | ~12MB | +| Project Type | Time taken | RAM taken | +|-----------------------------|------------|-----------| +| AML Modeling Framework (TS) | ~2s | ~100MB | +| go-raml | ~4ms | ~12MB | ## Installation +### Library + ``` go get -u github.com/acronis/go-raml ``` +### CLI + +Go install +``` +go install github.com/acronis/go-raml/cmd/raml +``` + +Make install +``` +make install +``` + ## Library usage examples ### Parser options -By default, parser outputs the resulting model as is. This means that information about all links and inheritance chains is unmodified. Be aware -that the parser may generate recursive structures, depending on your definition, and you may need to implement recursion detection with the model. +By default, parser outputs the resulting model as is. This means that information about all links and inheritance chains +is unmodified. Be aware +that the parser may generate recursive structures, depending on your definition, and you may need to implement recursion +detection with the model. The parser currently provides two options: -* `raml.OptWithValidate()` - performs validation of the resulting model (types inheritance validation, types facet validations, annotation types and instances validation, examples, defaults, instances, etc.). Also performs unwrap if `raml.OptWithUnwrap()` was not specified, but leaves the original model untouched. +* `raml.OptWithValidate()` - performs validation of the resulting model (types inheritance validation, types facet + validations, annotation types and instances validation, examples, defaults, instances, etc.). Also performs unwrap if + `raml.OptWithUnwrap()` was not specified, but leaves the original model untouched. -* `raml.OptWithUnwrap()` - performs an unwrap of the resulting model and replaces all definitions with unwrapped structures. Unwrap resolves the inheritance chain and links and compiles a complete type, with all properties of its parents/links. +* `raml.OptWithUnwrap()` - performs an unwrap of the resulting model and replaces all definitions with unwrapped + structures. Unwrap resolves the inheritance chain and links and compiles a complete type, with all properties of its + parents/links. ### Parsing from string -The following code will parse a RAML string, output a library model and print the common information about the defined type. +The following code will parse a RAML string, output a library model and print the common information about the defined +type. ```go package main import ( - "fmt" - "log" - "os" + "fmt" + "log" + "os" - "github.com/acronis/go-raml" + "github.com/acronis/go-raml" ) func main() { - // Get current working directory that will serve as a base path - workDir, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - - // Define RAML 1.0 Library in a string - content := `#%RAML 1.0 Library + // Get current working directory that will serve as a base path + workDir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + // Define RAML 1.0 Library in a string + content := `#%RAML 1.0 Library types: BasicType: string ChildType: @@ -153,27 +179,27 @@ types: minLength: 5 ` - // Parse with validation - r, err := raml.ParseFromString(content, "library.raml", workDir, raml.OptWithValidate()) - if err != nil { - log.Fatal(err) - } - // Cast type to Library since our fragment is RAML 1.0 Library - lib, _ := r.EntryPoint().(*raml.Library) - typPtr, _ := lib.Types.Get("ChildType") - // Cast type to StringShape since child type inherits from a string type - typ := (*typPtr).(*raml.StringShape) - fmt.Printf( - "Type name: %s, type: %s, minLength: %d, location: %s\n", - typ.Base().Name, typ.Base().Type, *typ.MinLength, typ.Base().Location, - ) - // Cast type to StringShape since parent type is string - parentTyp := (*typ.Base().Inherits[0]).(*raml.StringShape) - fmt.Printf("Inherits from:\n") - fmt.Printf( - "Type name: %s, type: %s, minLength: %d, location: %s\n", - parentTyp.Base().Name, parentTyp.Base().Type, parentTyp.MinLength, parentTyp.Base().Location, - ) + // Parse with validation + r, err := raml.ParseFromString(content, "library.raml", workDir, raml.OptWithValidate()) + if err != nil { + log.Fatal(err) + } + // Cast type to Library since our fragment is RAML 1.0 Library + lib, _ := r.EntryPoint().(*raml.Library) + typPtr, _ := lib.Types.Get("ChildType") + // Cast type to StringShape since child type inherits from a string type + typ := (*typPtr).(*raml.StringShape) + fmt.Printf( + "Type name: %s, type: %s, minLength: %d, location: %s\n", + typ.Base().Name, typ.Base().Type, *typ.MinLength, typ.Base().Location, + ) + // Cast type to StringShape since parent type is string + parentTyp := (*typ.Base().Inherits[0]).(*raml.StringShape) + fmt.Printf("Inherits from:\n") + fmt.Printf( + "Type name: %s, type: %s, minLength: %d, location: %s\n", + parentTyp.Base().Name, parentTyp.Base().Type, parentTyp.MinLength, parentTyp.Base().Location, + ) } ``` @@ -187,29 +213,30 @@ Type name: BasicType, type: string, minLength: 0, location: /libr ### Parsing from file -The following code will parse a RAML file, output a library model and print the common information about the defined type. +The following code will parse a RAML file, output a library model and print the common information about the defined +type. ```go package main import ( - "fmt" - "log" - "os" + "fmt" + "log" + "os" - "github.com/acronis/go-raml" + "github.com/acronis/go-raml" ) func main() { - filePath := "" - r, err := raml.ParseFromPath(content, "library.raml", workDir, raml.OptWithValidate()) - if err != nil { - log.Fatal(err) - } - lib, _ := r.EntryPoint().(*raml.Library) - typPtr, _ := lib.Types.Get("BasicType") - typ := *typPtr - fmt.Printf("Type name: %s, type: %s, location: %s", typ.Base().Name, typ.Base().Type, typ.Base().Location) + filePath := "" + r, err := raml.ParseFromPath(content, "library.raml", workDir, raml.OptWithValidate()) + if err != nil { + log.Fatal(err) + } + lib, _ := r.EntryPoint().(*raml.Library) + typPtr, _ := lib.Types.Get("BasicType") + typ := *typPtr + fmt.Printf("Type name: %s, type: %s, location: %s", typ.Base().Name, typ.Base().Type, typ.Base().Location) } ``` @@ -222,46 +249,46 @@ The following simple example demonstrates how a defined type can be used to vali package main import ( - "fmt" - "log" - "os" + "fmt" + "log" + "os" - "github.com/acronis/go-raml" + "github.com/acronis/go-raml" ) func main() { - // Get current working directory that will serve as a base path - workDir, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - - // Define RAML 1.0 Library in a string - content := `#%RAML 1.0 Library + // Get current working directory that will serve as a base path + workDir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + // Define RAML 1.0 Library in a string + content := `#%RAML 1.0 Library types: StringType: type: string minLength: 5 ` - // Parse with validation - r, err := raml.ParseFromString(content, "library.raml", workDir, raml.OptWithValidate()) - if err != nil { - log.Fatal(err) - } - // Cast type to Library since our fragment is RAML 1.0 Library - lib, _ := r.EntryPoint().(*raml.Library) - typPtr, _ := lib.Types.Get("StringType") - // Cast type to StringShape since defined type is a string - typ := (*typPtr).(*raml.StringShape) - fmt.Printf( - "Type name: %s, type: %s, minLength: %d, location: %s\n", - typ.Base().Name, typ.Base().Type, *typ.MinLength, typ.Base().Location, - ) - fmt.Printf("Empty string: %v\n", typ.Validate("", "$")) - fmt.Printf("Less than 5 characters: %v\n", typ.Validate("abc", "$")) - fmt.Printf("More than 5 characters: %v\n", typ.Validate("more than 5 chars", "$")) - fmt.Printf("Not a string: %v\n", typ.Validate(123, "$")) + // Parse with validation + r, err := raml.ParseFromString(content, "library.raml", workDir, raml.OptWithValidate()) + if err != nil { + log.Fatal(err) + } + // Cast type to Library since our fragment is RAML 1.0 Library + lib, _ := r.EntryPoint().(*raml.Library) + typPtr, _ := lib.Types.Get("StringType") + // Cast type to StringShape since defined type is a string + typ := (*typPtr).(*raml.StringShape) + fmt.Printf( + "Type name: %s, type: %s, minLength: %d, location: %s\n", + typ.Base().Name, typ.Base().Type, *typ.MinLength, typ.Base().Location, + ) + fmt.Printf("Empty string: %v\n", typ.Validate("", "$")) + fmt.Printf("Less than 5 characters: %v\n", typ.Validate("abc", "$")) + fmt.Printf("More than 5 characters: %v\n", typ.Validate("more than 5 chars", "$")) + fmt.Printf("Not a string: %v\n", typ.Validate(123, "$")) } ``` @@ -273,3 +300,146 @@ Less than 5 characters: length must be greater than 5 More than 5 characters: Not a string: invalid type, got int, expected string ``` + +## CLI usage examples + +Flags: +* `-v` `--verbosity count` - increase verbosity level, one flag for each level, e.g. `-v` for DEBUG +* `-d` `--ensure-duplicates` - ensure that there are no duplicates in tracebacks + +### Validate + +The `validate` command validates the RAML file against the RAML 1.0 specification. + +The following commands will validate the RAML files and output the validation errors. + +One file +```bash +raml validate .raml +``` + +Multiple files +```bash +raml validate .raml .raml .raml +``` + +Output example +``` +% raml validate library.raml +[11:46:40.053] INFO: Validating RAML... { + "path": "library.raml" +} +[11:46:40.060] ERROR: RAML is invalid { + "tracebacks": { + "traces": { + "0": { + "stack": { + "0": { + "message": "unwrap shapes", + "position": "/tmp/library.raml:1", + "severity": "error", + "type": "parsing" + }, + "1": { + "message": "unwrap shape", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "unwrapping" + }, + "2": { + "message": "merge shapes", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "unwrapping" + }, + "3": { + "message": "merge shapes", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "unwrapping" + }, + "4": { + "message": "inherit property: property: a", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + }, + "5": { + "message": "merge shapes", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + }, + "6": { + "message": "cannot inherit from different type: source: string: target: integer", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + } + } + }, + "1": { + "stack": { + "0": { + "message": "validate shapes", + "position": "/tmp/library.raml:1", + "severity": "error", + "type": "parsing" + }, + "1": { + "message": "check type", + "position": "/tmp/common.raml:8:5", + "severity": "error", + "type": "validating" + }, + "2": { + "message": "minProperties must be less than or equal to maxProperties", + "position": "/tmp/common.raml:8:5", + "severity": "error", + "type": "validating" + }, + "3": { + "message": "unwrap shape", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "validating" + }, + "4": { + "message": "merge shapes", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "unwrapping" + }, + "5": { + "message": "merge shapes", + "position": "/tmp/common.raml:15:5", + "severity": "error", + "type": "unwrapping" + }, + "6": { + "message": "inherit property: property: a", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + }, + "7": { + "message": "merge shapes", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + }, + "8": { + "message": "cannot inherit from different type: source: string: target: integer", + "position": "/tmp/common.raml:17:10", + "severity": "error", + "type": "unwrapping" + } + } + } + } + } +} +[11:46:40.092] ERROR: Command failed { + "error": "errors have been found in the RAML files" +} +``` \ No newline at end of file diff --git a/cmd/raml/go.mod b/cmd/raml/go.mod new file mode 100644 index 0000000..28a5b45 --- /dev/null +++ b/cmd/raml/go.mod @@ -0,0 +1,26 @@ +module github.com/acronis/go-raml/cmd/raml + +go 1.23.0 + +require ( + github.com/acronis/go-raml v0.9.0 + github.com/acronis/go-stacktrace v0.2.0 + github.com/dusted-go/logging v1.3.0 + github.com/samber/slog-formatter v1.1.0 + github.com/spf13/cobra v1.8.1 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/samber/lo v1.44.0 // indirect + github.com/samber/slog-multi v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cmd/raml/go.sum b/cmd/raml/go.sum new file mode 100644 index 0000000..aaaf31b --- /dev/null +++ b/cmd/raml/go.sum @@ -0,0 +1,45 @@ +github.com/acronis/go-raml v0.9.0 h1:5jvIInX44u9HDkqc7g7zJltUJsHaplQqG6aKcNnT50o= +github.com/acronis/go-raml v0.9.0/go.mod h1:NwovfR4e7ufFaoH77/3U0f82lu6soXlNc+vdFfpik4A= +github.com/acronis/go-stacktrace v0.2.0 h1:aUME2BnO2WwBpmidhSq+C2cCm6T0i7u1mwraetKPyjQ= +github.com/acronis/go-stacktrace v0.2.0/go.mod h1:FOvjPOpMOpJhNgt2adD+FEnOpzcOzUBeiRkPaAd2aLQ= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dusted-go/logging v1.3.0 h1:SL/EH1Rp27oJQIte+LjWvWACSnYDTqNx5gZULin0XRY= +github.com/dusted-go/logging v1.3.0/go.mod h1:s58+s64zE5fxSWWZfp+b8ZV0CHyKHjamITGyuY1wzGg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA= +github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/samber/slog-formatter v1.1.0 h1:9waVLjNnUWUac6OVv1cj9Y1RQAwo/LhAD3jMzXuaVzY= +github.com/samber/slog-formatter v1.1.0/go.mod h1:CEPmgdYDd+4lK0hbsxCkOVsLAJ4WXMhdUPypdwyNpLk= +github.com/samber/slog-multi v1.1.0 h1:m5wfpXE8Qu2gCiR/JnhFGsLcWDOmTxnso32EMffVAY0= +github.com/samber/slog-multi v1.1.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/raml/raml.go b/cmd/raml/raml.go new file mode 100644 index 0000000..c7397f2 --- /dev/null +++ b/cmd/raml/raml.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "os/signal" + + "github.com/acronis/go-stacktrace" + "github.com/spf13/cobra" +) + +type CommandError struct { + Inner error + Msg string +} + +func (e *CommandError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Inner) +} + +func (e *CommandError) Unwrap() error { + return e.Inner +} + +func NewCommandError(err error, msg string) error { + if err != nil { + return &CommandError{Inner: err, Msg: msg} + } + return nil +} + +type Command interface { + Execute(ctx context.Context) error +} + +func InitLoggingAndRun(ctx context.Context, verbosity int, cmd Command) error { + lvl := slog.LevelInfo + if verbosity > 0 { + lvl = slog.LevelDebug + } + InitLogging(lvl) + return NewCommandError(cmd.Execute(ctx), "command error") +} + +func main() { + os.Exit(mainFn()) +} + +func mainFn() int { + var ensureDuplicates bool + verbosity := 0 + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer stop() + + cmdValidate := func() *cobra.Command { + opts := ValidateOptions{ + EnsureDuplicates: ensureDuplicates, + } + cmd := &cobra.Command{ + Use: "validate", + Short: "validate raml files", + Args: cobra.MinimumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return InitLoggingAndRun(ctx, verbosity, NewValidateCmd(opts, args)) + }, + } + + return cmd + }() + + rootCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "raml", + Short: "raml is a RAML 1.0 tool", + SilenceUsage: true, + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + } + + cmd.PersistentFlags().CountVarP(&verbosity, "verbosity", "v", "increase verbosity level: -v for debug") + cmd.Flags().BoolVarP(&ensureDuplicates, "ensure-duplicates", "d", false, + "ensure that there are no duplicates in tracebacks") + + cmd.AddCommand( + cmdValidate, + ) + return cmd + }() + + if err := rootCmd.Execute(); err != nil { + var cmdErr *CommandError + if errors.As(err, &cmdErr) && cmdErr.Inner != nil { + stOpts := []stacktrace.TracesOpt{} + if ensureDuplicates { + stOpts = append(stOpts, stacktrace.WithEnsureDuplicates()) + } + slog.Error("Command failed", stacktrace.ErrToSlogAttr(cmdErr.Inner, stOpts...)) + } else { + _ = rootCmd.Usage() + } + return 1 + } + + return 0 +} diff --git a/cmd/raml/slog.go b/cmd/raml/slog.go new file mode 100644 index 0000000..3abc5e3 --- /dev/null +++ b/cmd/raml/slog.go @@ -0,0 +1,37 @@ +package main + +import ( + "log/slog" + "os" + "strings" + + "github.com/dusted-go/logging/prettylog" + slogformatter "github.com/samber/slog-formatter" +) + +func InitLogging(lvl slog.Level) { + logLvl := func() slog.Level { + return lvl + }() + w := os.Stderr + + funcHandler := slogformatter.NewFormatterHandler( + slogformatter.FormatByType(func(s []string) slog.Value { + return slog.StringValue(strings.Join(s, ",")) + }), + ) + + plHandler := prettylog.New( + &slog.HandlerOptions{ + Level: logLvl, + AddSource: false, + ReplaceAttr: nil, + }, + prettylog.WithDestinationWriter(w), + ) + + formatHandler := funcHandler(plHandler) + + logger := slog.New(formatHandler) + slog.SetDefault(logger) +} diff --git a/cmd/raml/validate.go b/cmd/raml/validate.go new file mode 100644 index 0000000..4ee6af8 --- /dev/null +++ b/cmd/raml/validate.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + + "github.com/acronis/go-raml" + "github.com/acronis/go-stacktrace" +) + +type ValidateOptions struct { + EnsureDuplicates bool +} + +type ValidateCommand struct { + Opts ValidateOptions + Args []string +} + +func NewValidateCmd(opts ValidateOptions, args []string) *ValidateCommand { + return &ValidateCommand{ + Opts: opts, + Args: args, + } +} + +func (v ValidateCommand) Execute(ctx context.Context) error { + var err error + var stOpts []stacktrace.TracesOpt + if v.Opts.EnsureDuplicates { + stOpts = append(stOpts, stacktrace.WithEnsureDuplicates()) + } + for _, arg := range v.Args { + slog.Info("Validating RAML...", slog.String("path", arg)) + _, err = raml.ParseFromPathCtx(ctx, arg, raml.OptWithUnwrap(), raml.OptWithValidate()) + if err != nil { + slog.Error("RAML is invalid", stacktrace.ErrToSlogAttr(err, stOpts...)) + } else { + slog.Info("RAML is valid", slog.String("path", arg)) + } + } + if err != nil { + return fmt.Errorf("errors have been found in the RAML files") + } + return nil +}