Skip to content

Commit 7ec21e8

Browse files
feat: added support for the Arazzo specification
0 parents  commit 7ec21e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+9886
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# github.com/speakeasy-api/openapi
2+
3+
The Speakeasy OpenAPI module provides a set of packages and tools for working with OpenAPI Specification documents.
4+
5+
## Main Packages
6+
7+
### [arazzo](./arazzo/README.md)
8+
9+
The `arazzo` package provides an API for working with Arazzo documents including reading, creating, mutating, walking and validating them.

arazzo/README.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# github.com/speakeasy-api/openapi/arazzo
2+
3+
The Arazzo package provides an API for working with Arazzo documents including reading, creating, mutating, walking and validating them.
4+
5+
For more details on the Arazzo specification see [Speakeasy's Arazzo Documentation](https://www.speakeasy.com/openapi/arazzo).
6+
7+
For details on the API available via the `arazzo` package see [https://pkg.go.dev/github.com/speakeasy-api/openapi/arazzo](https://pkg.go.dev/github.com/speakeasy-api/openapi/arazzo).
8+
9+
## Reading
10+
11+
```go
12+
package main
13+
14+
import (
15+
"context"
16+
"fmt"
17+
"os"
18+
19+
"github.com/speakeasy-api/openapi/arazzo"
20+
)
21+
22+
func main() {
23+
ctx := context.Background()
24+
25+
data, err := os.ReadFile("arazzo.yaml")
26+
if err != nil {
27+
panic(err)
28+
}
29+
30+
arazzo, validationErrs, err := arazzo.Unmarshal(ctx, data, "arazzo.yaml")
31+
if err != nil {
32+
panic(err)
33+
}
34+
35+
fmt.Printf("%+v\n", arazzo)
36+
fmt.Printf("%+v\n", validationErrs)
37+
}
38+
```
39+
40+
## Creating
41+
42+
```go
43+
package main
44+
45+
import (
46+
"context"
47+
"fmt"
48+
49+
"github.com/speakeasy-api/openapi/arazzo"
50+
)
51+
52+
func main() {
53+
ctx := context.Background()
54+
55+
arazzo := &arazzo.Arazzo{
56+
Arazzo: Version,
57+
Info: arazzo.Info{
58+
Title: "My Workflow",
59+
Summary: arazzo.pointer.From("A summary"),
60+
Version: "1.0.0",
61+
},
62+
// ...
63+
}
64+
65+
buf := bytes.NewBuffer([]byte{})
66+
67+
err := arazzo.Marshal(ctx, buf)
68+
if err != nil {
69+
panic(err)
70+
}
71+
72+
fmt.Printf("%s", buf.String())
73+
}
74+
```
75+
76+
## Mutating
77+
78+
```go
79+
package main
80+
81+
import (
82+
"context"
83+
"fmt"
84+
85+
"github.com/speakeasy-api/openapi/arazzo"
86+
)
87+
88+
func main() {
89+
ctx := context.Background()
90+
91+
data, err := os.ReadFile("arazzo.yaml")
92+
if err != nil {
93+
panic(err)
94+
}
95+
96+
arazzo, _, err := arazzo.Unmarshal(ctx, data, "arazzo.yaml")
97+
if err != nil {
98+
panic(err)
99+
}
100+
101+
arazzo.Info.Title = "My updated workflow title"
102+
103+
buf := bytes.NewBuffer([]byte{})
104+
105+
err := arazzo.Marshal(ctx, buf)
106+
if err != nil {
107+
panic(err)
108+
}
109+
110+
fmt.Printf("%s", buf.String())
111+
}
112+
```
113+
114+
## Walking
115+
116+
```go
117+
package main
118+
119+
import (
120+
"context"
121+
"fmt"
122+
"os"
123+
124+
"github.com/speakeasy-api/openapi/arazzo"
125+
)
126+
127+
func main() {
128+
ctx := context.Background()
129+
130+
data, err := os.ReadFile("arazzo.yaml")
131+
if err != nil {
132+
panic(err)
133+
}
134+
135+
arazzo, _, err := arazzo.Unmarshal(ctx, data, "arazzo.yaml")
136+
if err != nil {
137+
panic(err)
138+
}
139+
140+
err = arazzo.Walk(ctx, func(ctx context.Context, node, parent arazzo.MatchFunc, arazzo *arazzo.Arazzo) error {
141+
return node(arazzo.Matcher{
142+
Workflow: func(workflow *arazzo.Workflow) error {
143+
fmt.Printf("Workflow: %s\n", workflow.WorkflowID)
144+
return nil
145+
},
146+
})
147+
})
148+
if err != nil {
149+
panic(err)
150+
}
151+
}
152+
```
153+
154+
## Validating
155+
156+
```go
157+
package main
158+
159+
import (
160+
"context"
161+
"fmt"
162+
"os"
163+
164+
"github.com/speakeasy-api/openapi/arazzo"
165+
)
166+
167+
func main() {
168+
ctx := context.Background()
169+
170+
data, err := os.ReadFile("arazzo.yaml")
171+
if err != nil {
172+
panic(err)
173+
}
174+
175+
arazzo, validationErrs, err := arazzo.Unmarshal(ctx, data, "arazzo.yaml")
176+
if err != nil {
177+
panic(err)
178+
}
179+
180+
for _, err := range validationErrs {
181+
fmt.Printf("%s\n", err.Error())
182+
}
183+
}
184+
```

arazzo/arazzo.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// package arazzo provides an API for working with Arazzo documents including reading, creating, mutating, walking and validating them.
2+
//
3+
// The Arazzo Specification is a mechanism for orchestrating API calls, defining their sequences and dependencies, to achieve specific outcomes when working with API descriptions like OpenAPI.
4+
package arazzo
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"slices"
12+
13+
"github.com/speakeasy-api/openapi/arazzo/core"
14+
"github.com/speakeasy-api/openapi/extensions"
15+
"github.com/speakeasy-api/openapi/marshaller"
16+
"github.com/speakeasy-api/openapi/validation"
17+
"github.com/speakeasy-api/openapi/yml"
18+
)
19+
20+
// Version is the version of the Arazzo Specification that this package conforms to.
21+
const Version = "1.0.0"
22+
23+
// Arazzo is the root object for an Arazzo document.
24+
type Arazzo struct {
25+
// Arazzo is the version of the Arazzo Specification that this document conforms to.
26+
Arazzo string
27+
// Info provides metadata about the Arazzo document.
28+
Info Info
29+
// SourceDescriptions provides a list of SourceDescription objects that describe the source of the data that the workflow is orchestrating.
30+
SourceDescriptions SourceDescriptions
31+
// Workflows provides a list of Workflow objects that describe the orchestration of API calls.
32+
Workflows Workflows
33+
// Components provides a list of reusable components that can be used in the workflow.
34+
Components *Components
35+
// Extensions provides a list of extensions to the Arazzo document.
36+
Extensions *extensions.Extensions
37+
38+
core core.Arazzo
39+
}
40+
41+
var _ model[core.Arazzo] = (*Arazzo)(nil)
42+
43+
type Option[T any] func(o *T)
44+
45+
type unmarshalOptions struct {
46+
skipValidation bool
47+
}
48+
49+
// WithSkipValidation will skip validation of the Arazzo document during unmarshalling.
50+
// Useful to quickly load a document that will be mutated and validated later.
51+
func WithSkipValidation() Option[unmarshalOptions] {
52+
return func(o *unmarshalOptions) {
53+
o.skipValidation = true
54+
}
55+
}
56+
57+
// Unmarshal will unmarshal and validate an Arazzo document from the provided io.Reader.
58+
// Validation can be skipped by using arazzo.WithSkipValidation() as one of the options when calling this function.
59+
func Unmarshal(ctx context.Context, doc io.Reader, opts ...Option[unmarshalOptions]) (*Arazzo, []error, error) {
60+
o := unmarshalOptions{}
61+
for _, opt := range opts {
62+
opt(&o)
63+
}
64+
65+
ctx = validation.ContextWithValidationContext(ctx)
66+
67+
c, err := core.Unmarshal(ctx, doc)
68+
if err != nil {
69+
return nil, nil, err
70+
}
71+
72+
arazzo := &Arazzo{}
73+
if err := marshaller.PopulateModel(*c, arazzo); err != nil {
74+
return nil, nil, err
75+
}
76+
arazzo.core = *c
77+
78+
var validationErrs []error
79+
if !o.skipValidation {
80+
validationErrs = validation.GetValidationErrors(ctx)
81+
validationErrs = append(validationErrs, arazzo.Validate(ctx)...)
82+
slices.SortFunc(validationErrs, func(a, b error) int {
83+
var aValidationErr *validation.Error
84+
var bValidationErr *validation.Error
85+
aIsValidationErr := errors.As(a, &aValidationErr)
86+
bIsValidationErr := errors.As(b, &bValidationErr)
87+
if aIsValidationErr && bIsValidationErr {
88+
if aValidationErr.Line == bValidationErr.Line {
89+
return aValidationErr.Column - bValidationErr.Column
90+
}
91+
return aValidationErr.Line - bValidationErr.Line
92+
} else if aIsValidationErr {
93+
return -1
94+
} else if bIsValidationErr {
95+
return 1
96+
}
97+
98+
return 0
99+
})
100+
}
101+
102+
return arazzo, validationErrs, nil
103+
}
104+
105+
// Marshal will marshal the provided Arazzo document to the provided io.Writer.
106+
func Marshal(ctx context.Context, arazzo *Arazzo, w io.Writer) error {
107+
if arazzo == nil {
108+
return errors.New("nil *Arazzo")
109+
}
110+
111+
if err := arazzo.Marshal(ctx, w); err != nil {
112+
return err
113+
}
114+
115+
return nil
116+
}
117+
118+
// GetCore will return the low level representation of the Arazzo document.
119+
// Useful for accessing line and column numbers for various nodes in the backing yaml/json document.
120+
func (a *Arazzo) GetCore() *core.Arazzo {
121+
return &a.core
122+
}
123+
124+
// Marshal will marshal the Arazzo document to the provided io.Writer.
125+
func (a *Arazzo) Marshal(ctx context.Context, w io.Writer) error {
126+
ctx = yml.ContextWithConfig(ctx, a.core.Config)
127+
128+
if _, err := marshaller.SyncValue(ctx, a, &a.core, nil); err != nil {
129+
return err
130+
}
131+
132+
return a.core.Marshal(ctx, w)
133+
}
134+
135+
// Validate will validate the Arazzo document against the Arazzo Specification.
136+
func (a *Arazzo) Validate(ctx context.Context, opts ...validation.Option) []error {
137+
opts = append(opts, validation.WithContextObject(a))
138+
139+
errs := []error{}
140+
141+
if a.Arazzo != Version {
142+
errs = append(errs, &validation.Error{
143+
Message: "Arazzo version must be 1.0.0",
144+
Line: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Line,
145+
Column: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Column,
146+
})
147+
}
148+
149+
errs = append(errs, a.Info.Validate(ctx, opts...)...)
150+
151+
sourceDescriptionNames := make(map[string]bool)
152+
153+
for i, sourceDescription := range a.SourceDescriptions {
154+
errs = append(errs, sourceDescription.Validate(ctx, opts...)...)
155+
156+
if _, ok := sourceDescriptionNames[sourceDescription.Name]; ok {
157+
errs = append(errs, &validation.Error{
158+
Message: fmt.Sprintf("sourceDescription name %s is not unique", sourceDescription.Name),
159+
Line: a.core.SourceDescriptions.GetSliceValueNodeOrRoot(i, a.core.RootNode).Line,
160+
Column: a.core.SourceDescriptions.GetSliceValueNodeOrRoot(i, a.core.RootNode).Column,
161+
})
162+
}
163+
164+
sourceDescriptionNames[sourceDescription.Name] = true
165+
}
166+
167+
workflowIds := make(map[string]bool)
168+
169+
for i, workflow := range a.Workflows {
170+
errs = append(errs, workflow.Validate(ctx, opts...)...)
171+
172+
if _, ok := workflowIds[workflow.WorkflowID]; ok {
173+
errs = append(errs, &validation.Error{
174+
Message: fmt.Sprintf("workflowId %s is not unique", workflow.WorkflowID),
175+
Line: a.core.Workflows.GetSliceValueNodeOrRoot(i, a.core.RootNode).Line,
176+
Column: a.core.Workflows.GetSliceValueNodeOrRoot(i, a.core.RootNode).Column,
177+
})
178+
}
179+
180+
workflowIds[workflow.WorkflowID] = true
181+
}
182+
183+
if a.Components != nil {
184+
errs = append(errs, a.Components.Validate(ctx, opts...)...)
185+
}
186+
187+
return errs
188+
}

0 commit comments

Comments
 (0)