diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..06a9200 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +EPCC CLI (`epcc`) is a command-line tool for interacting with the Elastic Path Composable Commerce API. Built with Go and the Cobra CLI framework, it dynamically generates CRUD commands from YAML resource definitions. + +## Build & Test Commands + +```bash +# Build +make build # Output: ./bin/epcc + +# Run all unit tests +go test -v -cover ./cmd/ ./external/... + +# Run a single test +go test -v -run TestName ./cmd/ +go test -v -run TestName ./external/packagename/ + +# Format check (CI enforces this) +gofmt -s -l . + +# Format fix +go fmt "./..." + +# Smoke tests (require built binary in PATH and API credentials) +export PATH=./bin/:$PATH +./cmd/get-all-smoke-tests.sh +./external/runbooks/run-all-runbooks.sh +``` + +## Architecture + +### Command Generation Pattern + +The CLI dynamically generates commands at startup rather than hardcoding each one: + +1. **`main.go`** → `cmd.InitializeCmd()` → `cmd.Execute()` +2. **`cmd/root.go`**: `InitializeCmd()` loads resource YAML definitions, then calls `NewCreateCommand()`, `NewGetCommand()`, `NewUpdateCommand()`, `NewDeleteCommand()`, etc. to build the command tree +3. Each `New*Command()` function (in `cmd/create.go`, `cmd/get.go`, etc.) iterates over all resources and generates a subcommand per resource + +### Resource Definitions (`external/resources/`) + +Resources are defined in `external/resources/yaml/*.yaml` files, embedded via `//go:embed`. Each YAML file maps a resource type to its API endpoints, field definitions, and autofill capabilities. The `Resource` struct in `external/resources/` is the central type that drives command generation, REST calls, and completion. + +#### Resources Definitions Vs. OpenAPI Specs + +The Resource Definitions predate OpenAPI specs and have historically been incorrect, a lot of work has been done to make them better, and so they are more authoritative. The resource definitions can express different things more easily or harder than the OpenAPI specs, +for instance while there is one platform, the OpenAPI specs are fragmented, and don't semantically link resources, for instance the + +### Request Flow + +``` +cmd/{create,get,update,delete}.go → external/rest/{create,get,update,delete}.go + → external/httpclient/ → external/authentication/ (token management) + → HTTP request to API +``` + +### Key Packages + +- **`external/httpclient/`** - HTTP client with rate limiting, retries (5xx, 429), custom headers, URL rewriting, and request/response logging +- **`external/authentication/`** - Multiple auth flows (Client Credentials, Customer Token, Account Management, OIDC) with token caching +- **`external/runbooks/`** - YAML-based action sequences with Go template rendering, variable systems, and parallel execution +- **`external/aliases/`** - Named references to resource IDs for scripting +- **`external/profiles/`** - Context isolation for multiple environments +- **`external/json/`** - JQ integration for output post-processing + +### Design Decision: Loose OpenAPI Dependency + +OpenAPI specs are included in the repo but the CLI does not depend on them at runtime. Resource definitions are duplicated in the YAML configs. The tool should build and work without specs; they're primarily used for validation in tests. + +The resources definitions are designed to simplify interacting with EPCC via the command line, and as such are more concise. + +## Code Style + +- Go standard formatting (`gofmt -s`), tabs for Go, 2-space indent for YAML +- Tests use `stretchr/testify` (`require` package) +- No linter beyond `gofmt` is configured + +## Configuration + +API credentials and CLI behavior are controlled via environment variables prefixed with `EPCC_` (defined in `config/config.go`). Key ones: `EPCC_CLIENT_ID`, `EPCC_CLIENT_SECRET`, `EPCC_API_BASE_URL`. Profile-based context isolation is available via `EPCC_PROFILE`. diff --git a/external/completion/completion_test.go b/external/completion/completion_test.go index 13aedba..6b50025 100644 --- a/external/completion/completion_test.go +++ b/external/completion/completion_test.go @@ -885,7 +885,7 @@ func TestCompleteAttributeKeyWithWhenSkippingWhen(t *testing.T) { require.Contains(t, completions, "validation.string.regex") require.Contains(t, completions, "validation.integer.allow_null_values") require.Contains(t, completions, "validation.integer.immutable") - require.Len(t, completions, 18) + require.Len(t, completions, 33) } func TestCompleteQueryParamKeyGetCollectionWithExplicitParams(t *testing.T) { diff --git a/external/resources/yaml/commerce-extensions.yaml b/external/resources/yaml/commerce-extensions.yaml index 8506546..c60a101 100644 --- a/external/resources/yaml/commerce-extensions.yaml +++ b/external/resources/yaml/commerce-extensions.yaml @@ -21,6 +21,15 @@ custom-apis: get-collection: docs: "https://elasticpath.dev/docs/api/commerce-extensions/list-custom-ap-is" url: "/v2/settings/extensions/custom-apis" + query: + - name: page[offset] + type: INT + - name: page[limit] + type: INT + - name: filter + type: STRING + - name: sort + type: ENUM:id,-id,created_at,-created_at,updated_at,-updated_at,api_type,-api_type,name,-name,slug,-slug attributes: name: type: STRING @@ -32,6 +41,12 @@ custom-apis: description: type: STRING autofill: FUNC:Phrase + allow_upserts: + type: BOOL + presentation.page: + type: STRING + presentation.section: + type: STRING relationships.parent_apis[n].type: type: ENUM:api_location,custom_api relationships.parent_apis[n].id: @@ -56,6 +71,15 @@ custom-fields: get-collection: docs: "https://elasticpath.dev/docs/api/commerce-extensions/list-custom-fields" url: "/v2/settings/extensions/custom-apis/{custom_apis}/fields/" + query: + - name: page[offset] + type: INT + - name: page[limit] + type: INT + - name: filter + type: STRING + - name: sort + type: ENUM:id,-id,created_at,-created_at,updated_at,-updated_at,field_type,-field_type,name,-name,slug,-slug attributes: name: type: STRING @@ -63,7 +87,7 @@ custom-fields: slug: type: STRING field_type: - type: ENUM:string,integer,boolean,float + type: ENUM:string,integer,boolean,float,any,list description: type: STRING autofill: FUNC:Phrase @@ -111,6 +135,51 @@ custom-fields: validation.boolean.immutable: type: BOOL when: field_type == "boolean" + validation.float.min_value: + type: INT + when: field_type == "float" + validation.float.max_value: + type: INT + when: field_type == "float" + validation.float.allow_null_values: + type: BOOL + when: field_type == "float" + validation.float.immutable: + type: BOOL + when: field_type == "float" + validation.any.allow_null_values: + type: BOOL + when: field_type == "any" + validation.any.immutable: + type: BOOL + when: field_type == "any" + validation.any.json_schema.version: + type: STRING + when: field_type == "any" + validation.any.json_schema.schema: + type: STRING + when: field_type == "any" + validation.list.allow_null_values: + type: BOOL + when: field_type == "list" + validation.list.immutable: + type: BOOL + when: field_type == "list" + validation.list.min_length: + type: INT + when: field_type == "list" + validation.list.max_length: + type: INT + when: field_type == "list" + validation.list.allowed_type: + type: ENUM:any,string,integer,boolean,float + when: field_type == "list" + validation.list.json_schema.version: + type: STRING + when: field_type == "list" + validation.list.json_schema.schema: + type: STRING + when: field_type == "list" custom-api-settings-entries: singular-name: custom-api-settings-entry json-api-type: custom_entry @@ -134,6 +203,17 @@ custom-api-settings-entries: get-collection: docs: "https://elasticpath.dev/docs/api/commerce-extensions/list-custom-api-entries" url: "/v2/settings/extensions/custom-apis/{custom_apis}/entries/" + query: + - name: page[offset] + type: INT + - name: page[limit] + type: INT + - name: filter + type: STRING + - name: sort + type: ENUM:id,-id,created_at,-created_at,updated_at,-updated_at + - name: timeout + type: INT attributes: data.type: type: STRING @@ -173,6 +253,17 @@ custom-api-extensions-entries: url: "/v2/extensions/{custom_apis}/" parent_resource_value_overrides: custom_apis: slug + query: + - name: page[offset] + type: INT + - name: page[limit] + type: INT + - name: filter + type: STRING + - name: sort + type: ENUM:id,-id,created_at,-created_at,updated_at,-updated_at + - name: timeout + type: INT attributes: data.type: type: STRING diff --git a/external/resources/yaml/resources_yaml_test.go b/external/resources/yaml/resources_yaml_test.go index bd91091..95159cb 100644 --- a/external/resources/yaml/resources_yaml_test.go +++ b/external/resources/yaml/resources_yaml_test.go @@ -21,7 +21,6 @@ import ( "github.com/quasilyte/regex/syntax" "github.com/santhosh-tekuri/jsonschema/v4" log "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yosida95/uritemplate/v3" "gopkg.in/yaml.v3" @@ -380,6 +379,7 @@ var redirectRegex = regexp.MustCompile(`window\.location\.href\s*=\s*'([^']+)'`) var titleRegex = regexp.MustCompile(`