← back to Cli Printing Press
feat(spec): YAML spec parser with validation and Stytch test fixture
b4ab56ba6c58477cfa439b67fcd8a551b6543b7c · 2026-03-23 09:03:22 -0700 · Matt Van Horn
Parses API description YAML into Go structs. Validates required fields,
auth config, resource endpoints. Tests pass against Stytch fixture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M go.modM go.sumA internal/spec/spec_test.goA testdata/stytch.yaml
Diff
commit b4ab56ba6c58477cfa439b67fcd8a551b6543b7c
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Mon Mar 23 09:03:22 2026 -0700
feat(spec): YAML spec parser with validation and Stytch test fixture
Parses API description YAML into Go structs. Validates required fields,
auth config, resource endpoints. Tests pass against Stytch fixture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
go.mod | 3 ++
go.sum | 6 +++
internal/spec/spec_test.go | 87 ++++++++++++++++++++++++++++++
testdata/stytch.yaml | 131 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 227 insertions(+)
diff --git a/go.mod b/go.mod
index d46c2082..1fd7224f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,10 +4,13 @@ go 1.26.1
require (
github.com/spf13/cobra v1.10.2
+ github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)
require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)
diff --git a/go.sum b/go.sum
index 47edb24d..5352f4f3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,17 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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=
diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go
new file mode 100644
index 00000000..7be0e6d1
--- /dev/null
+++ b/internal/spec/spec_test.go
@@ -0,0 +1,87 @@
+package spec
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseStytch(t *testing.T) {
+ s, err := Parse("../../testdata/stytch.yaml")
+ require.NoError(t, err)
+
+ assert.Equal(t, "stytch", s.Name)
+ assert.Equal(t, "Stytch authentication API CLI", s.Description)
+ assert.Equal(t, "0.1.0", s.Version)
+ assert.Equal(t, "https://api.stytch.com/v1", s.BaseURL)
+
+ // Auth
+ assert.Equal(t, "api_key", s.Auth.Type)
+ assert.Equal(t, "Authorization", s.Auth.Header)
+ assert.Len(t, s.Auth.EnvVars, 2)
+
+ // Resources
+ assert.Len(t, s.Resources, 2)
+ users := s.Resources["users"]
+ assert.Equal(t, "Manage Stytch users", users.Description)
+ assert.Len(t, users.Endpoints, 4) // list, get, create, delete
+
+ // Users list endpoint
+ list := users.Endpoints["list"]
+ assert.Equal(t, "GET", list.Method)
+ assert.Equal(t, "/users", list.Path)
+ assert.NotNil(t, list.Pagination)
+ assert.Equal(t, "cursor", list.Pagination.Type)
+
+ // Users create endpoint
+ create := users.Endpoints["create"]
+ assert.Equal(t, "POST", create.Method)
+ assert.Len(t, create.Body, 2)
+
+ // Sessions
+ sessions := s.Resources["sessions"]
+ assert.Len(t, sessions.Endpoints, 2)
+
+ // Types
+ assert.Len(t, s.Types, 2)
+ assert.Len(t, s.Types["User"].Fields, 5)
+ assert.Len(t, s.Types["Session"].Fields, 4)
+}
+
+func TestValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ spec APISpec
+ wantErr string
+ }{
+ {
+ name: "empty name",
+ spec: APISpec{BaseURL: "http://x", Resources: map[string]Resource{"a": {Endpoints: map[string]Endpoint{"b": {Method: "GET", Path: "/"}}}}},
+ wantErr: "name is required",
+ },
+ {
+ name: "empty base_url",
+ spec: APISpec{Name: "x", Resources: map[string]Resource{"a": {Endpoints: map[string]Endpoint{"b": {Method: "GET", Path: "/"}}}}},
+ wantErr: "base_url is required",
+ },
+ {
+ name: "no resources",
+ spec: APISpec{Name: "x", BaseURL: "http://x"},
+ wantErr: "at least one resource is required",
+ },
+ {
+ name: "endpoint missing method",
+ spec: APISpec{Name: "x", BaseURL: "http://x", Resources: map[string]Resource{"a": {Endpoints: map[string]Endpoint{"b": {Path: "/"}}}}},
+ wantErr: "method is required",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.spec.Validate()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.wantErr)
+ })
+ }
+}
diff --git a/testdata/stytch.yaml b/testdata/stytch.yaml
new file mode 100644
index 00000000..91f3fdb1
--- /dev/null
+++ b/testdata/stytch.yaml
@@ -0,0 +1,131 @@
+name: stytch
+description: "Stytch authentication API CLI"
+version: "0.1.0"
+base_url: "https://api.stytch.com/v1"
+
+auth:
+ type: api_key
+ header: "Authorization"
+ format: "Basic {project_id}:{secret}"
+ env_vars:
+ - STYTCH_PROJECT_ID
+ - STYTCH_SECRET
+
+config:
+ format: toml
+ path: "~/.config/stytch-cli/config.toml"
+
+resources:
+ users:
+ description: "Manage Stytch users"
+ endpoints:
+ list:
+ method: GET
+ path: "/users"
+ description: "List all users"
+ params:
+ - name: limit
+ type: int
+ default: 100
+ description: "Max users to return"
+ - name: cursor
+ type: string
+ description: "Pagination cursor"
+ response:
+ type: array
+ item: User
+ pagination:
+ type: cursor
+ cursor_field: "cursor"
+ has_more_field: "results.has_more"
+
+ get:
+ method: GET
+ path: "/users/{user_id}"
+ description: "Get a user by ID"
+ params:
+ - name: user_id
+ type: string
+ required: true
+ positional: true
+ description: "User ID"
+ response:
+ type: object
+ item: User
+
+ create:
+ method: POST
+ path: "/users"
+ description: "Create a new user"
+ body:
+ - name: email
+ type: string
+ description: "User email"
+ - name: phone_number
+ type: string
+ description: "User phone number"
+ response:
+ type: object
+ item: User
+
+ delete:
+ method: DELETE
+ path: "/users/{user_id}"
+ description: "Delete a user"
+ params:
+ - name: user_id
+ type: string
+ required: true
+ positional: true
+ description: "User ID"
+
+ sessions:
+ description: "Manage user sessions"
+ endpoints:
+ list:
+ method: GET
+ path: "/sessions"
+ description: "List sessions for a user"
+ params:
+ - name: user_id
+ type: string
+ required: true
+ description: "User ID"
+ response:
+ type: array
+ item: Session
+
+ revoke:
+ method: POST
+ path: "/sessions/revoke"
+ description: "Revoke a session"
+ body:
+ - name: session_id
+ type: string
+ required: true
+ description: "Session ID to revoke"
+
+types:
+ User:
+ fields:
+ - name: user_id
+ type: string
+ - name: email
+ type: string
+ - name: phone_number
+ type: string
+ - name: status
+ type: string
+ - name: created_at
+ type: string
+
+ Session:
+ fields:
+ - name: session_id
+ type: string
+ - name: user_id
+ type: string
+ - name: started_at
+ type: string
+ - name: expires_at
+ type: string
← 454739bd feat(scaffold): initial project structure with CLI skeleton
·
back to Cli Printing Press
·
feat(templates): Go templates for all generated CLI files a78f0979 →