Skip to content

Commit

Permalink
fix: add support for arazzo specification 1.0.1 (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
TristanSpeakEasy authored Feb 24, 2025
1 parent 92ded3c commit 3b71427
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 37 deletions.
46 changes: 43 additions & 3 deletions arazzo/arazzo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"fmt"
"io"
"slices"
"strconv"
"strings"

"github.com/speakeasy-api/openapi/arazzo/core"
"github.com/speakeasy-api/openapi/extensions"
Expand All @@ -18,7 +20,12 @@ import (
)

// Version is the version of the Arazzo Specification that this package conforms to.
const Version = "1.0.0"
const (
Version = "1.0.1"
VersionMajor = 1
VersionMinor = 0
VersionPatch = 1
)

// Arazzo is the root object for an Arazzo document.
type Arazzo struct {
Expand Down Expand Up @@ -148,9 +155,18 @@ func (a *Arazzo) Validate(ctx context.Context, opts ...validation.Option) []erro

errs := []error{}

if a.Arazzo != Version {
arazzoMajor, arazzoMinor, arazzoPatch, err := parseVersion(a.Arazzo)
if err != nil {
errs = append(errs, &validation.Error{
Message: "Arazzo version must be 1.0.0",
Message: fmt.Sprintf("invalid Arazzo version in document %s: %s", a.Arazzo, err.Error()),
Line: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Line,
Column: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Column,
})
}

if arazzoMajor != VersionMajor || arazzoMinor != VersionMinor || arazzoPatch > VersionPatch {
errs = append(errs, &validation.Error{
Message: fmt.Sprintf("Only Arazzo version %s and below is supported", Version),
Line: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Line,
Column: a.core.Arazzo.GetValueNodeOrRoot(a.core.RootNode).Column,
})
Expand Down Expand Up @@ -200,3 +216,27 @@ func (a *Arazzo) Validate(ctx context.Context, opts ...validation.Option) []erro

return errs
}

func parseVersion(version string) (int, int, int, error) {
parts := strings.Split(version, ".")
if len(parts) != 3 {
return 0, 0, 0, fmt.Errorf("invalid version %s", version)
}

major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid major version %s: %w", parts[0], err)
}

minor, err := strconv.Atoi(parts[1])
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid minor version %s: %w", parts[1], err)
}

patch, err := strconv.Atoi(parts[2])
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid patch version %s: %w", parts[2], err)
}

return major, minor, patch, nil
}
2 changes: 1 addition & 1 deletion arazzo/arazzo_examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func Example_creating() {
Info: arazzo.Info{
Title: "My Workflow",
Summary: pointer.From("A summary"),
Version: "1.0.0",
Version: "1.0.1",
},
// ...
}
Expand Down
45 changes: 18 additions & 27 deletions arazzo/arazzo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func TestArazzo_RoundTrip_Success(t *testing.T) {
}

func TestArazzoUnmarshal_ValidationErrors(t *testing.T) {
data := []byte(`arazzo: 1.0.1
data := []byte(`arazzo: 1.0.2
x-test: some-value
info:
title: My Workflow
Expand All @@ -254,14 +254,14 @@ sourceDescriptions:

assert.Equal(t, []error{
&validation.Error{Line: 1, Column: 1, Message: "field workflows is missing"},
&validation.Error{Line: 1, Column: 9, Message: "Arazzo version must be 1.0.0"},
&validation.Error{Line: 1, Column: 9, Message: "Only Arazzo version 1.0.1 and below is supported"},
&validation.Error{Line: 4, Column: 3, Message: "field version is missing"},
&validation.Error{Line: 6, Column: 5, Message: "field url is missing"},
&validation.Error{Line: 7, Column: 11, Message: "type must be one of [openapi, arazzo]"},
}, validationErrs)

expected := &arazzo.Arazzo{
Arazzo: "1.0.1",
Arazzo: "1.0.2",
Info: arazzo.Info{
Title: "My Workflow",
Version: "",
Expand Down Expand Up @@ -307,6 +307,7 @@ func TestArazzo_Mutate_Success(t *testing.T) {
require.NoError(t, err)
require.Empty(t, validationErrs)

a.Arazzo = "1.0.0"
a.Info.Title = "My updated workflow title"
sd := a.SourceDescriptions[0]
sd.Extensions.Set("x-test", yml.CreateOrUpdateScalarNode(ctx, "some-value", nil))
Expand Down Expand Up @@ -478,53 +479,49 @@ var stressTests = []struct {
{
name: "Redocly Museum API",
args: args{
location: "https://raw.githubusercontent.com/Redocly/museum-openapi-example/091a853a0d2467bc4c65bb6f615a33d0a7201747/arazzo/museum-api.arazzo.yaml",
validationIgnores: []string{
"invalid jsonpath expression", // they have criterion marked as jsonpath but uses a simple condition instead
},
location: "https://raw.githubusercontent.com/Redocly/museum-openapi-example/2770b2b2e59832d245c7b0eb0badf6568d7efb53/arazzo/museum-api.arazzo.yaml",
},
wantTitle: "Redocly Museum API Test Workflow",
},
{
name: "Redocly Museum Tickets",
args: args{
location: "https://raw.githubusercontent.com/Redocly/museum-openapi-example/091a853a0d2467bc4c65bb6f615a33d0a7201747/arazzo/museum-tickets.arazzo.yaml",
location: "https://raw.githubusercontent.com/Redocly/museum-openapi-example/2770b2b2e59832d245c7b0eb0badf6568d7efb53/arazzo/museum-tickets.arazzo.yaml",
},
wantTitle: "Redocly Museum Tickets Workflow",
},
{
name: "Redocly Warp API",
args: args{
// TODO line 25 report inconsistency with spec and value
location: "https://raw.githubusercontent.com/Redocly/warp-single-sidebar/be5f885db3cdd9c595f9a7e724c04e9f6a0b70dd/apis/arazzo.yaml",
location: "https://raw.githubusercontent.com/Redocly/warp-single-sidebar/b78fc09da52d7755e92e1bc8f990edd37421cbde/apis/arazzo.yaml",
},
wantTitle: "Warp API",
},
{
name: "Arazzo Extended Parameters Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/ExtendedParametersExample.arazzo.yaml",
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/ExtendedParametersExample.arazzo.yaml",
},
wantTitle: "Public Zoo API",
},
{
name: "Arazzo FAPI-PAR Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/FAPI-PAR.arazzo.yaml",
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/FAPI-PAR.arazzo.yaml",
},
wantTitle: "PAR, Authorization and Token workflow",
},
{
name: "Arazzo Login and Retrieve Pets Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/LoginAndRetrievePets.arazzo.yaml",
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/LoginAndRetrievePets.arazzo.yaml",
},
wantTitle: "A pet purchasing workflow",
},
{
name: "Arazzo BNPL Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/bnpl-arazzo.yaml",
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/bnpl-arazzo.yaml",
validationIgnores: []string{
"$response.headers.Location", // doc should be referencing `$response.header.Location`
},
Expand All @@ -534,18 +531,14 @@ var stressTests = []struct {
{
name: "Arazzo OAuth Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/oauth.arazzo.yaml",
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/oauth.arazzo.yaml",
},
wantTitle: "Example OAuth service",
},
{
name: "Arazzo Pet Coupons Example",
args: args{
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/977f586da14b65bd8e612b763267b8b728749e52/examples/1.0.0/pet-coupons.arazzo.yaml",
validationIgnores: []string{
"$outputs[0]", // legit issue trying to reference outputs by index
"$workflow_order_id", // legit issue trying to reference workflow_order_id
},
location: "https://raw.githubusercontent.com/OAI/Arazzo-Specification/23852b8b0d13ab1e3288a57a990611ffed45ab5d/examples/1.0.0/pet-coupons.arazzo.yaml",
},
wantTitle: "Petstore - Apply Coupons",
},
Expand Down Expand Up @@ -582,7 +575,7 @@ var stressTests = []struct {
{
name: "Itarazzo Client Pet Store Example",
args: args{
location: "https://raw.githubusercontent.com/leidenheit/itarazzo-client/b744ca1ca3a036964ae30be601f10a25b14dc52d/src/test/resources/pet-store.arazzo.yaml",
location: "https://raw.githubusercontent.com/leidenheit/itarazzo-client/b3c126d28bf80ae7d74861c08509be33b83c5ddf/src/test/resources/pet-store.arazzo.yaml",
validationIgnores: []string{
"jsonpointer must start with /: $.status", // legit issues TODO: improve the error returned as it is wrong
"jsonpointer must start with /: $.id", // legit issues TODO: improve the error returned as it is wrong
Expand All @@ -593,15 +586,14 @@ var stressTests = []struct {
{
name: "Ritza build-a-bot workflow",
args: args{
location: "https://raw.githubusercontent.com/ritza-co/e2e-testing-arazzo/c0615c3708a1e4c0fcaeb79edae78ddc4eb5ba82/arazzo.yaml",
validationIgnores: []string{},
location: "https://raw.githubusercontent.com/ritza-co/e2e-testing-arazzo/c0615c3708a1e4c0fcaeb79edae78ddc4eb5ba82/arazzo.yaml",
},
wantTitle: "Build-a-Bot Workflow",
},
{
name: " API-Flows adyen-giving workflow",
args: args{
location: "https://raw.githubusercontent.com/API-Flows/openapi-workflow-registry/3d85d79232fa8f42993b2f5bd47e273b9369dc2d/root/adyen/adyen-giving.yaml",
location: "https://raw.githubusercontent.com/API-Flows/openapi-workflow-registry/75c237ce1b155ba9f8dc7f065759df7ae1cbbbe5/root/adyen/adyen-giving.yaml",
validationIgnores: []string{
"in must be one of [path, query, header, cookie] but was body",
},
Expand All @@ -611,16 +603,15 @@ var stressTests = []struct {
{
name: "API-Flows simple workflow",
args: args{
location: "https://raw.githubusercontent.com/API-Flows/openapi-workflow-parser/6b28ba4def262969c5a96bc54d08433e6c336643/src/test/resources/1.0.0/simple.workflow.yaml",
validationIgnores: []string{},
location: "https://raw.githubusercontent.com/API-Flows/openapi-workflow-parser/6b28ba4def262969c5a96bc54d08433e6c336643/src/test/resources/1.0.0/simple.workflow.yaml",
},
wantTitle: "simple",
},
// Disabled for now as it is currently failing round tripping due to missing conditions
// {
// name: "Kartikhub swap tokens workflow",
// args: args{
// location: "https://raw.githubusercontent.com/Kartikhub/web3-basics/d95bc51bb935ef07d627e52c6fdfe18aaea69e18/swap-react/docs/swap-transaction-arazzo.yaml",
// location: "https://raw.githubusercontent.com/Kartikhub/web3-basics/be13fa7e6fdf386eef08bba2843d4a8b615561b9/swap-react/docs/swap-transaction-arazzo.yaml",
// validationIgnores: []string{ // All valid issues
// "field condition is missing",
// "condition is required",
Expand Down
10 changes: 9 additions & 1 deletion arazzo/expression/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expression
import (
"fmt"
"regexp"
"slices"
"strings"

"github.com/speakeasy-api/openapi/jsonpointer"
Expand Down Expand Up @@ -150,7 +151,14 @@ func (e Expression) Validate(validateAsExpression bool) error {
return err
}

if typ == ExpressionTypeSourceDescriptions && strings.HasSuffix(name, "url") {
switch {
case typ == ExpressionTypeSourceDescriptions && strings.HasSuffix(name, "url"):
allowJsonPointers = true
case slices.Contains([]ExpressionType{ExpressionTypeSteps, ExpressionTypeWorkflows}, typ):
if len(expressionParts) > 0 && expressionParts[0] == "outputs" {
allowJsonPointers = true
}
case typ == ExpressionTypeOutputs:
allowJsonPointers = true
}
default:
Expand Down
34 changes: 31 additions & 3 deletions arazzo/expression/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,41 @@ func TestExpression_Validate_Success(t *testing.T) {
validateAsExpression: true,
},
},
{
name: "outputs with json pointer",
args: args{
e: Expression("$outputs.someOutput#/some/path"),
validateAsExpression: true,
},
},
{
name: "steps",
args: args{
e: Expression("$steps.someStep"),
validateAsExpression: true,
},
},
{
name: "step outputs with json pointer",
args: args{
e: Expression("$steps.someStep.outputs.someOutput#/some/path"),
validateAsExpression: true,
},
},
{
name: "workflows",
args: args{
e: Expression("$workflows.someWorkflow"),
validateAsExpression: true,
},
},
{
name: "workflow outputs with json pointer",
args: args{
e: Expression("$workflows.someWorkflow.outputs.someOutput#/some/path"),
validateAsExpression: true,
},
},
{
name: "source descriptions",
args: args{
Expand Down Expand Up @@ -335,11 +356,18 @@ func TestExpression_Validate_Failure(t *testing.T) {
wantErr: errors.New("expression is not valid, expected name after $inputs: $inputs"),
},
{
name: "invalid json pointer expression in context",
name: "invalid json pointer expression in inputs expression",
args: args{
e: Expression("$inputs.someInput#/some/path"),
},
wantErr: errors.New("expression is not valid, json pointers are not allowed in current context: $inputs.someInput#/some/path"),
},
{
name: "invalid json pointer expression in workflow inputs expression",
args: args{
e: Expression("$outputs.someOutput#/some/path"),
e: Expression("$workflows.someWorkflow.inputs.someInput#/some/path"),
},
wantErr: errors.New("expression is not valid, json pointers are not allowed in current context: $outputs.someOutput#/some/path"),
wantErr: errors.New("expression is not valid, json pointers are not allowed in current context: $workflows.someWorkflow.inputs.someInput#/some/path"),
},
}
for _, tt := range tests {
Expand Down
2 changes: 1 addition & 1 deletion arazzo/expression/expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestExtractExpressions(t *testing.T) {
},
},
{
name: "expression with json pointer",
name: "request body expression with json pointer",
args: args{
expression: "$request.body#/some/path",
},
Expand Down
2 changes: 1 addition & 1 deletion arazzo/testdata/test.arazzo.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
arazzo: 1.0.0
arazzo: 1.0.1
info:
title: My Workflow
summary: A summary
Expand Down

0 comments on commit 3b71427

Please sign in to comment.