Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add support for arazzo specification 1.0.1 #10

Merged
merged 2 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading