diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ee49ac2..fd0ccba 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.11" + ".": "0.1.0-alpha.12" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 0e44408..d304fb3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-5ca77894755b023e347be818cea03fc11810d3de5df5167772a7ab44111d9413.yml -openapi_spec_hash: e1df8ec98c9bcc6d93c9bafa8ff79d29 -config_hash: a7461e35260504dcac4cb8c3bfcaad72 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-2b3066c21238e834fda5197e6afbbfaaf41e189a1cc9b3c14a892b90d5a209ba.yml +openapi_spec_hash: 96d0b5cd724b242a0943129fdc79bdca +config_hash: 6ae0d102d10842637680db3e63e42c51 diff --git a/CHANGELOG.md b/CHANGELOG.md index 509241b..60175c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.1.0-alpha.12 (2025-06-16) + +Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.11...v0.1.0-alpha.12) + +### Features + +* add build/upload steps to builds api ([4573934](https://github.com/stainless-api/stainless-api-cli/commit/45739343a28e9615e62e0c5126d1d66715212df9)) +* add platform headers ([ca381b4](https://github.com/stainless-api/stainless-api-cli/commit/ca381b436607c3a7801b74e91a9147a2b59378bc)) +* **api:** add v0 project create api ([a40eca6](https://github.com/stainless-api/stainless-api-cli/commit/a40eca6419d7ab8159491e861cde86d3e779b0a0)) +* **api:** manual updates ([ef33c30](https://github.com/stainless-api/stainless-api-cli/commit/ef33c30512ca63d63380eb3f7c118e315696b2bc)) +* **api:** manual updates ([80a6fb5](https://github.com/stainless-api/stainless-api-cli/commit/80a6fb50820cd4766359bcf74daccf72f44fad59)) + + +### Bug Fixes + +* changes har request format for snippets API some more ([4b2a898](https://github.com/stainless-api/stainless-api-cli/commit/4b2a898f947b7a4e000e758eb28cea5c00950687)) +* fix type errors ([d5e1ae3](https://github.com/stainless-api/stainless-api-cli/commit/d5e1ae3f6c2566a42400a0a18394266a700f7463)) + + +### Chores + +* bump go package to 0.6.0 ([36a231d](https://github.com/stainless-api/stainless-api-cli/commit/36a231d0c7cb28e4111e9cbc3ac3c78833a340be)) +* **internal:** codegen related update ([a108d1a](https://github.com/stainless-api/stainless-api-cli/commit/a108d1ad99e6bfd3469a8cecf48b4b299b199796)) +* **internal:** codegen related update ([894c558](https://github.com/stainless-api/stainless-api-cli/commit/894c558dac1b497c69e1696453b9672bb923a0d9)) + + +### Refactors + +* move build_target_outputs to builds.target_outputs ([a085509](https://github.com/stainless-api/stainless-api-cli/commit/a08550995142a7f45786b9e33b3d20360068f2ec)) + ## 0.1.0-alpha.11 (2025-06-02) Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.10...v0.1.0-alpha.11) diff --git a/go.mod b/go.mod index 9222991..6707c6e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/stainless-api/stainless-api-cli go 1.23.0 -toolchain go1.23.9 +toolchain go1.23.10 require ( github.com/charmbracelet/bubbles v0.21.0 @@ -10,7 +10,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/logrusorgru/aurora/v4 v4.0.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/stainless-api/stainless-api-go v0.5.1 + github.com/stainless-api/stainless-api-go v0.7.0 github.com/tidwall/gjson v1.17.0 github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 diff --git a/go.sum b/go.sum index b2ea9a8..ca00f5d 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stainless-api/stainless-api-go v0.5.1 h1:+y/D5G9x7aaPA7AAkXhYSwKUhftz2Mj3D1MzDCymt3k= -github.com/stainless-api/stainless-api-go v0.5.1/go.mod h1:9Q2t8xq6EFgw8HYOsVxqKEfSDVe9eqCoh1zC0HMRwTY= +github.com/stainless-api/stainless-api-go v0.7.0 h1:nT4DT9qo7IvnhSnJs6aekGhiW662aeWeN34f/JiQBpA= +github.com/stainless-api/stainless-api-go v0.7.0/go.mod h1:9Q2t8xq6EFgw8HYOsVxqKEfSDVe9eqCoh1zC0HMRwTY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 87fb67d..3443d4a 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -13,6 +13,7 @@ import ( "path/filepath" "time" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" "github.com/urfave/cli/v3" @@ -96,27 +97,35 @@ var buildsCreate = cli.Command{ Name: "create", Usage: "Create a new build", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "project", - Action: getAPIFlagAction[string]("body", "project"), + &jsonflag.JSONStringFlag{ + Name: "project", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "project", + }, }, - &cli.StringFlag{ - Name: "revision", - Action: getAPIFlagAction[string]("body", "revision"), + &jsonflag.JSONStringFlag{ + Name: "revision", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "revision", + }, }, - &cli.StringFlag{ + &jsonflag.JSONFileFlag{ Name: "openapi-spec", Aliases: []string{"oas"}, - Action: getAPIFlagFileAction("body", "revision.openapi\\.yml.content"), + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "revision.openapi\\.yml.content", + }, }, - &cli.StringFlag{ + &jsonflag.JSONFileFlag{ Name: "stainless-config", Aliases: []string{"config"}, - Action: getAPIFlagFileAction("body", "revision.openapi\\.stainless\\.yml.content"), - }, - &cli.BoolFlag{ - Name: "allow-empty", - Action: getAPIFlagAction[bool]("body", "allow_empty"), + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "revision.openapi\\.stainless\\.yml.content", + }, }, &cli.BoolFlag{ Name: "wait", @@ -126,24 +135,42 @@ var buildsCreate = cli.Command{ Name: "pull", Usage: "Pull the build outputs after completion (only works with --wait)", }, - &cli.StringFlag{ - Name: "branch", - Action: getAPIFlagAction[string]("body", "branch"), + &jsonflag.JSONBoolFlag{ + Name: "allow-empty", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "allow_empty", + }, }, - &cli.StringFlag{ - Name: "commit-message", - Action: getAPIFlagAction[string]("body", "commit_message"), + &jsonflag.JSONStringFlag{ + Name: "branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "branch", + }, }, - &cli.StringFlag{ - Name: "targets", - Action: getAPIFlagAction[string]("body", "targets.#"), + &jsonflag.JSONStringFlag{ + Name: "commit-message", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "commit_message", + }, }, - &cli.StringFlag{ - Name: "+target", - Action: getAPIFlagAction[string]("body", "targets.-1"), + &jsonflag.JSONStringFlag{ + Name: "targets", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.#", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "+target", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.-1", + }, }, }, - Before: initAPICommandWithWorkspaceDefaults, Action: handleBuildsCreate, HideHelpCommand: true, } @@ -156,7 +183,6 @@ var buildsRetrieve = cli.Command{ Name: "build-id", }, }, - Before: initAPICommand, Action: handleBuildsRetrieve, HideHelpCommand: true, } @@ -165,28 +191,42 @@ var buildsList = cli.Command{ Name: "list", Usage: "List builds for a project", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "project", - Action: getAPIFlagAction[string]("query", "project"), + &jsonflag.JSONStringFlag{ + Name: "project", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "project", + }, }, - &cli.StringFlag{ - Name: "branch", - Action: getAPIFlagAction[string]("query", "branch"), + &jsonflag.JSONStringFlag{ + Name: "branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "branch", + }, }, - &cli.StringFlag{ - Name: "cursor", - Action: getAPIFlagAction[string]("query", "cursor"), + &jsonflag.JSONStringFlag{ + Name: "cursor", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "cursor", + }, }, - &cli.FloatFlag{ - Name: "limit", - Action: getAPIFlagAction[float64]("query", "limit"), + &jsonflag.JSONFloatFlag{ + Name: "limit", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "limit", + }, }, - &cli.StringFlag{ - Name: "revision", - Action: getAPIFlagAction[string]("query", "revision"), + &jsonflag.JSONStringFlag{ + Name: "revision", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "revision", + }, }, }, - Before: initAPICommand, Action: handleBuildsList, HideHelpCommand: true, } @@ -195,51 +235,76 @@ var buildsCompare = cli.Command{ Name: "compare", Usage: "Creates two builds whose outputs can be compared directly", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "base.revision", - Action: getAPIFlagAction[string]("body", "base.revision"), + &jsonflag.JSONStringFlag{ + Name: "base.revision", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "base.revision", + }, }, - &cli.StringFlag{ - Name: "base.branch", - Action: getAPIFlagAction[string]("body", "base.branch"), + &jsonflag.JSONStringFlag{ + Name: "base.branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "base.branch", + }, }, - &cli.StringFlag{ - Name: "base.commit_message", - Action: getAPIFlagAction[string]("body", "base.commit_message"), + &jsonflag.JSONStringFlag{ + Name: "base.commit_message", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "base.commit_message", + }, }, - &cli.StringFlag{ - Name: "head.revision", - Action: getAPIFlagAction[string]("body", "head.revision"), + &jsonflag.JSONStringFlag{ + Name: "head.revision", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "head.revision", + }, }, - &cli.StringFlag{ - Name: "head.branch", - Action: getAPIFlagAction[string]("body", "head.branch"), + &jsonflag.JSONStringFlag{ + Name: "head.branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "head.branch", + }, }, - &cli.StringFlag{ - Name: "head.commit_message", - Action: getAPIFlagAction[string]("body", "head.commit_message"), + &jsonflag.JSONStringFlag{ + Name: "head.commit_message", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "head.commit_message", + }, }, - &cli.StringFlag{ - Name: "project", - Action: getAPIFlagAction[string]("body", "project"), + &jsonflag.JSONStringFlag{ + Name: "project", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "project", + }, }, - &cli.StringFlag{ - Name: "targets", - Action: getAPIFlagAction[string]("body", "targets.#"), + &jsonflag.JSONStringFlag{ + Name: "targets", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.#", + }, }, - &cli.StringFlag{ - Name: "+target", - Action: getAPIFlagAction[string]("body", "targets.-1"), + &jsonflag.JSONStringFlag{ + Name: "+target", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.-1", + }, }, }, - Before: initAPICommand, Action: handleBuildsCompare, HideHelpCommand: true, } func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) - // Log to stderr that we're creating a build + cc := getAPICommandContext(cmd) fmt.Fprintf(os.Stderr, "%s Creating build...\n", au.BrightCyan("✱")) params := stainlessv0.BuildNewParams{} res, err := cc.client.Builds.New( @@ -339,7 +404,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { } func handleBuildsRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) res, err := cc.client.Builds.Get( context.TODO(), cmd.Value("build-id").(string), @@ -378,7 +443,7 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl au.BrightCyan("✱"), i+1, len(targets), au.Bold(target), au.Cyan(targetDir)) // Get the output details - outputRes, err := client.BuildTargetOutputs.Get( + outputRes, err := client.Builds.TargetOutputs.Get( ctx, stainlessv0.BuildTargetOutputGetParams{ BuildID: res.ID, @@ -394,7 +459,7 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl } // Handle based on output type - err = pullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, targetDir, target) + err = pullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, targetDir) if err != nil { fmt.Fprintf(os.Stderr, "%s Failed to pull %s: %v\n", au.BrightRed("✱"), target, err) @@ -413,7 +478,7 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl } // pullOutput handles downloading or cloning a build target output -func pullOutput(output, url, ref, targetDir, target string) error { +func pullOutput(output, url, ref, targetDir string) error { // Remove existing directory if it exists if _, err := os.Stat(targetDir); err == nil { fmt.Fprintf(os.Stderr, " %s Removing existing directory %s\n", au.BrightBlack("•"), targetDir) @@ -497,7 +562,7 @@ func pullOutput(output, url, ref, targetDir, target string) error { } func handleBuildsList(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.BuildListParams{} res, err := cc.client.Builds.List( context.TODO(), @@ -513,7 +578,7 @@ func handleBuildsList(ctx context.Context, cmd *cli.Command) error { } func handleBuildsCompare(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.BuildCompareParams{} res, err := cc.client.Builds.Compare( context.TODO(), @@ -528,12 +593,9 @@ func handleBuildsCompare(ctx context.Context, cmd *cli.Command) error { return nil } -// initAPICommandWithWorkspaceDefaults applies workspace defaults before initializing API command -func initAPICommandWithWorkspaceDefaults(ctx context.Context, cmd *cli.Command) (context.Context, error) { - cc, err := initAPICommand(ctx, cmd) - if err != nil { - return nil, err - } +// getAPICommandWithWorkspaceDefaults applies workspace defaults before initializing API command +func getAPICommandContextWithWorkspaceDefaults(cmd *cli.Command) (*apiCommandContext, error) { + cc := getAPICommandContext(cmd) config, configPath, err := FindWorkspaceConfig() if err == nil && config != nil { // Get the directory containing the workspace config file @@ -542,19 +604,21 @@ func initAPICommandWithWorkspaceDefaults(ctx context.Context, cmd *cli.Command) if !cmd.IsSet("openapi-spec") && !cmd.IsSet("oas") && config.OpenAPISpec != "" { // Resolve OpenAPI spec path relative to workspace config directory openAPIPath := filepath.Join(configDir, config.OpenAPISpec) - fileAction := getAPIFlagFileAction("body", "revision.openapi\\.yml.content") - if err := fileAction(cc, cmd, openAPIPath); err != nil { + content, err := os.ReadFile(openAPIPath) + if err != nil { return nil, fmt.Errorf("failed to load OpenAPI spec from workspace config: %v", err) } + jsonflag.Register(jsonflag.Body, "revision.openapi\\.yml.content", string(content)) } if !cmd.IsSet("stainless-config") && !cmd.IsSet("config") && config.StainlessConfig != "" { // Resolve Stainless config path relative to workspace config directory stainlessConfigPath := filepath.Join(configDir, config.StainlessConfig) - fileAction := getAPIFlagFileAction("body", "revision.openapi\\.stainless\\.yml.content") - if err := fileAction(cc, cmd, stainlessConfigPath); err != nil { + content, err := os.ReadFile(stainlessConfigPath) + if err != nil { return nil, fmt.Errorf("failed to load Stainless config from workspace config: %v", err) } + jsonflag.Register(jsonflag.Body, "revision.openapi\\.stainless\\.yml.content", string(content)) } } return cc, err diff --git a/pkg/cmd/buildtargetoutput.go b/pkg/cmd/buildtargetoutput.go index 0c4aa04..03da217 100644 --- a/pkg/cmd/buildtargetoutput.go +++ b/pkg/cmd/buildtargetoutput.go @@ -7,67 +7,55 @@ import ( "fmt" "os" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" "github.com/urfave/cli/v3" ) -var buildTargetOutputsRetrieve = cli.Command{ +var buildsTargetOutputsRetrieve = cli.Command{ Name: "retrieve", Usage: "Download the output of a build target", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "build-id", - Action: getAPIFlagAction[string]("query", "build_id"), + &cli.BoolFlag{ + Name: "pull", }, - &cli.StringFlag{ - Name: "target", - Action: getAPIFlagAction[string]("query", "target"), + &jsonflag.JSONStringFlag{ + Name: "build-id", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "build_id", + }, }, - &cli.StringFlag{ - Name: "type", - Action: getAPIFlagAction[string]("query", "type"), + &jsonflag.JSONStringFlag{ + Name: "target", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "target", + }, }, - &cli.StringFlag{ - Name: "output", - Action: getAPIFlagAction[string]("query", "output"), + &jsonflag.JSONStringFlag{ + Name: "type", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "type", + }, }, - }, - Before: initAPICommand, - Action: handleBuildTargetOutputsPull, - HideHelpCommand: true, -} - -var buildTargetOutputsPull = cli.Command{ - Name: "pull", - Usage: "TODO", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "build-id", - Action: getAPIFlagAction[string]("query", "build_id"), - }, - &cli.StringFlag{ - Name: "target", - Action: getAPIFlagAction[string]("query", "target"), - }, - &cli.StringFlag{ - Name: "type", - Action: getAPIFlagAction[string]("query", "type"), - }, - &cli.StringFlag{ - Name: "output", - Action: getAPIFlagAction[string]("query", "output"), + &jsonflag.JSONStringFlag{ + Name: "output", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "output", + }, }, }, - Before: initAPICommand, - Action: handleBuildTargetOutputsPull, - HideHelpCommand: true, + Action: handleBuildsTargetOutputsRetrieve, } -func handleBuildTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) +func handleBuildsTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) params := stainlessv0.BuildTargetOutputGetParams{} - res, err := cc.client.BuildTargetOutputs.Get( + res, err := cc.client.Builds.TargetOutputs.Get( context.TODO(), params, option.WithMiddleware(cc.AsMiddleware()), @@ -77,23 +65,14 @@ func handleBuildTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) err } fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) - return nil -} -func handleBuildTargetOutputsPull(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) - - res, err := cc.client.BuildTargetOutputs.Get( - context.TODO(), - stainlessv0.BuildTargetOutputGetParams{}, - option.WithMiddleware(cc.AsMiddleware()), - ) - if err != nil { - return err + if cmd.Bool("pull") { + build, err := cc.client.Builds.Get(ctx, cmd.String("build-id")) + if err != nil { + return err + } + targetDir := fmt.Sprintf("%s-%s", build.Project, cmd.String("target")) + return pullOutput(res.Output, res.URL, res.Ref, targetDir) } - - targetDir := fmt.Sprintf("%s-%s", "tmp", "target") - - // Use the shared pullOutput function - return pullOutput(res.Output, res.URL, res.Ref, targetDir, "target") + return nil } diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index a04a1ba..48f6d75 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -29,6 +29,7 @@ var Command = cli.Command{ { Name: "projects", Commands: []*cli.Command{ + &projectsCreate, &projectsRetrieve, &projectsUpdate, &projectsList, @@ -51,13 +52,6 @@ var Command = cli.Command{ }, }, - { - Name: "projects:snippets", - Commands: []*cli.Command{ - &projectsSnippetsCreateRequest, - }, - }, - { Name: "builds", Commands: []*cli.Command{ @@ -69,9 +63,9 @@ var Command = cli.Command{ }, { - Name: "build_target_outputs", + Name: "builds:target_outputs", Commands: []*cli.Command{ - &buildTargetOutputsRetrieve, + &buildsTargetOutputsRetrieve, }, }, diff --git a/pkg/cmd/org.go b/pkg/cmd/org.go index be3adff..84d2fd7 100644 --- a/pkg/cmd/org.go +++ b/pkg/cmd/org.go @@ -16,10 +16,9 @@ var orgsRetrieve = cli.Command{ Usage: "Retrieve an organization by name", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "org-name", + Name: "org", }, }, - Before: initAPICommand, Action: handleOrgsRetrieve, HideHelpCommand: true, } @@ -28,16 +27,15 @@ var orgsList = cli.Command{ Name: "list", Usage: "List organizations the user has access to", Flags: []cli.Flag{}, - Before: initAPICommand, Action: handleOrgsList, HideHelpCommand: true, } func handleOrgsRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) res, err := cc.client.Orgs.Get( context.TODO(), - cmd.Value("org-name").(string), + cmd.Value("org").(string), option.WithMiddleware(cc.AsMiddleware()), ) if err != nil { @@ -49,7 +47,7 @@ func handleOrgsRetrieve(ctx context.Context, cmd *cli.Command) error { } func handleOrgsList(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) res, err := cc.client.Orgs.List(context.TODO(), option.WithMiddleware(cc.AsMiddleware())) if err != nil { return err diff --git a/pkg/cmd/project.go b/pkg/cmd/project.go index 54d6ab4..7dde655 100644 --- a/pkg/cmd/project.go +++ b/pkg/cmd/project.go @@ -7,11 +7,56 @@ import ( "fmt" "os" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" "github.com/urfave/cli/v3" ) +var projectsCreate = cli.Command{ + Name: "create", + Usage: "Create a new project", + Flags: []cli.Flag{ + &jsonflag.JSONStringFlag{ + Name: "display-name", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "display_name", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "org", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "org", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "slug", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "slug", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "targets", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.#", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "+target", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "targets.-1", + }, + }, + }, + Action: handleProjectsCreate, + HideHelpCommand: true, +} + var projectsRetrieve = cli.Command{ Name: "retrieve", Usage: "Retrieve a project by name", @@ -20,7 +65,6 @@ var projectsRetrieve = cli.Command{ Name: "project", }, }, - Before: initAPICommand, Action: handleProjectsRetrieve, HideHelpCommand: true, } @@ -32,12 +76,14 @@ var projectsUpdate = cli.Command{ &cli.StringFlag{ Name: "project", }, - &cli.StringFlag{ - Name: "display-name", - Action: getAPIFlagAction[string]("body", "display_name"), + &jsonflag.JSONStringFlag{ + Name: "display-name", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "display_name", + }, }, }, - Before: initAPICommand, Action: handleProjectsUpdate, HideHelpCommand: true, } @@ -46,26 +92,51 @@ var projectsList = cli.Command{ Name: "list", Usage: "List projects in an organization", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cursor", - Action: getAPIFlagAction[string]("query", "cursor"), + &jsonflag.JSONStringFlag{ + Name: "cursor", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "cursor", + }, }, - &cli.FloatFlag{ - Name: "limit", - Action: getAPIFlagAction[float64]("query", "limit"), + &jsonflag.JSONFloatFlag{ + Name: "limit", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "limit", + }, }, - &cli.StringFlag{ - Name: "org", - Action: getAPIFlagAction[string]("query", "org"), + &jsonflag.JSONStringFlag{ + Name: "org", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "org", + }, }, }, - Before: initAPICommand, Action: handleProjectsList, HideHelpCommand: true, } +func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + params := stainlessv0.ProjectNewParams{} + res, err := cc.client.Projects.New( + context.TODO(), + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithRequestBody("application/json", cc.body), + ) + if err != nil { + return err + } + + fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + return nil +} + func handleProjectsRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectGetParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) @@ -84,7 +155,7 @@ func handleProjectsRetrieve(ctx context.Context, cmd *cli.Command) error { } func handleProjectsUpdate(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectUpdateParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) @@ -104,7 +175,7 @@ func handleProjectsUpdate(ctx context.Context, cmd *cli.Command) error { } func handleProjectsList(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectListParams{} res, err := cc.client.Projects.List( context.TODO(), diff --git a/pkg/cmd/projectbranch.go b/pkg/cmd/projectbranch.go index 589c2af..f169554 100644 --- a/pkg/cmd/projectbranch.go +++ b/pkg/cmd/projectbranch.go @@ -7,6 +7,7 @@ import ( "fmt" "os" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" "github.com/urfave/cli/v3" @@ -19,20 +20,28 @@ var projectsBranchesCreate = cli.Command{ &cli.StringFlag{ Name: "project", }, - &cli.StringFlag{ - Name: "branch", - Action: getAPIFlagAction[string]("body", "branch"), + &jsonflag.JSONStringFlag{ + Name: "branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "branch", + }, }, - &cli.StringFlag{ - Name: "branch-from", - Action: getAPIFlagAction[string]("body", "branch_from"), + &jsonflag.JSONStringFlag{ + Name: "branch-from", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "branch_from", + }, }, - &cli.BoolFlag{ - Name: "force", - Action: getAPIFlagAction[bool]("body", "force"), + &jsonflag.JSONBoolFlag{ + Name: "force", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "force", + }, }, }, - Before: initAPICommand, Action: handleProjectsBranchesCreate, HideHelpCommand: true, } @@ -48,13 +57,12 @@ var projectsBranchesRetrieve = cli.Command{ Name: "branch", }, }, - Before: initAPICommand, Action: handleProjectsBranchesRetrieve, HideHelpCommand: true, } func handleProjectsBranchesCreate(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectBranchNewParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) @@ -74,7 +82,7 @@ func handleProjectsBranchesCreate(ctx context.Context, cmd *cli.Command) error { } func handleProjectsBranchesRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectBranchGetParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) diff --git a/pkg/cmd/projectconfig.go b/pkg/cmd/projectconfig.go index a4484ee..b6efac9 100644 --- a/pkg/cmd/projectconfig.go +++ b/pkg/cmd/projectconfig.go @@ -7,6 +7,7 @@ import ( "fmt" "os" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" "github.com/urfave/cli/v3" @@ -19,12 +20,14 @@ var projectsConfigsRetrieve = cli.Command{ &cli.StringFlag{ Name: "project", }, - &cli.StringFlag{ - Name: "branch", - Action: getAPIFlagAction[string]("query", "branch"), + &jsonflag.JSONStringFlag{ + Name: "branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "branch", + }, }, }, - Before: initAPICommand, Action: handleProjectsConfigsRetrieve, HideHelpCommand: true, } @@ -36,22 +39,27 @@ var projectsConfigsGuess = cli.Command{ &cli.StringFlag{ Name: "project", }, - &cli.StringFlag{ - Name: "spec", - Action: getAPIFlagAction[string]("body", "spec"), + &jsonflag.JSONStringFlag{ + Name: "spec", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "spec", + }, }, - &cli.StringFlag{ - Name: "branch", - Action: getAPIFlagAction[string]("body", "branch"), + &jsonflag.JSONStringFlag{ + Name: "branch", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "branch", + }, }, }, - Before: initAPICommand, Action: handleProjectsConfigsGuess, HideHelpCommand: true, } func handleProjectsConfigsRetrieve(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectConfigGetParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) @@ -72,7 +80,7 @@ func handleProjectsConfigsRetrieve(ctx context.Context, cmd *cli.Command) error } func handleProjectsConfigsGuess(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) + cc := getAPICommandContext(cmd) params := stainlessv0.ProjectConfigGuessParams{} if cmd.IsSet("project") { params.Project = stainlessv0.String(cmd.Value("project").(string)) diff --git a/pkg/cmd/projectsnippet.go b/pkg/cmd/projectsnippet.go deleted file mode 100644 index 733f932..0000000 --- a/pkg/cmd/projectsnippet.go +++ /dev/null @@ -1,84 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - "os" - - "github.com/stainless-api/stainless-api-go" - "github.com/stainless-api/stainless-api-go/option" - "github.com/urfave/cli/v3" -) - -var projectsSnippetsCreateRequest = cli.Command{ - Name: "create_request", - Usage: "Perform create_request operation", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "project-name", - }, - &cli.StringFlag{ - Name: "request.method", - Action: getAPIFlagAction[string]("body", "request.method"), - }, - &cli.StringFlag{ - Name: "request.parameters.in", - Action: getAPIFlagAction[string]("body", "request.parameters.#.in"), - }, - &cli.StringFlag{ - Name: "request.parameters.name", - Action: getAPIFlagAction[string]("body", "request.parameters.#.name"), - }, - &cli.BoolFlag{ - Name: "request.+parameter", - Action: getAPIFlagActionWithValue[bool]("body", "request.parameters.-1", map[string]interface{}{}), - }, - &cli.StringFlag{ - Name: "request.path", - Action: getAPIFlagAction[string]("body", "request.path"), - }, - &cli.StringFlag{ - Name: "request.body.fileParam", - Action: getAPIFlagAction[string]("body", "request.body.fileParam"), - }, - &cli.StringFlag{ - Name: "request.body.filePath", - Action: getAPIFlagAction[string]("body", "request.body.filePath"), - }, - &cli.BoolFlag{ - Name: "har", - Action: getAPIFlagActionWithValue[bool]("body", "har", nil), - }, - &cli.StringFlag{ - Name: "language", - Action: getAPIFlagAction[string]("body", "language"), - }, - &cli.StringFlag{ - Name: "version", - Action: getAPIFlagAction[string]("body", "version"), - }, - }, - Before: initAPICommand, - Action: handleProjectsSnippetsCreateRequest, - HideHelpCommand: true, -} - -func handleProjectsSnippetsCreateRequest(ctx context.Context, cmd *cli.Command) error { - cc := getAPICommandContext(ctx, cmd) - params := stainlessv0.ProjectSnippetNewRequestParams{} - res, err := cc.client.Projects.Snippets.NewRequest( - context.TODO(), - cmd.Value("project-name").(string), - params, - option.WithMiddleware(cc.AsMiddleware()), - option.WithRequestBody("application/json", cc.body), - ) - if err != nil { - return err - } - - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) - return nil -} diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go index d5d67bd..efd65ed 100644 --- a/pkg/cmd/util.go +++ b/pkg/cmd/util.go @@ -1,46 +1,33 @@ package cmd import ( - "context" "fmt" "io" "log" "net/http" "net/url" "os" - "strconv" "strings" "github.com/logrusorgru/aurora/v4" + "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" + "github.com/tidwall/sjson" "github.com/tidwall/gjson" "github.com/tidwall/pretty" - "github.com/tidwall/sjson" "github.com/urfave/cli/v3" "golang.org/x/term" ) -func jsonSet(json []byte, path string, value interface{}) ([]byte, error) { - keys := strings.Split(path, ".") - path = "" - for i := 0; i < len(keys); i++ { - key := keys[i] - if key == "#" { - key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) - } - - if len(path) > 0 { - path += "." - } - path += key +func getDefaultRequestOptions() []option.RequestOption { + return []option.RequestOption{ + option.WithHeader("X-Stainless-Lang", "cli"), + option.WithHeader("X-Stainless-Runtime", "cli"), } - return sjson.SetBytes(json, path, value) } -type apiCommandKey string - type apiCommandContext struct { client stainlessv0.Client body []byte @@ -104,9 +91,8 @@ func (c apiCommandContext) AsMiddleware() option.Middleware { } } -func initAPICommand(ctx context.Context, cmd *cli.Command) (context.Context, error) { - client := stainlessv0.NewClient(getClientOptions(ctx, cmd)...) - +func getAPICommandContext(cmd *cli.Command) *apiCommandContext { + client := stainlessv0.NewClient(getDefaultRequestOptions()...) body := getStdInput() if body == nil { body = []byte("{}") @@ -114,66 +100,13 @@ func initAPICommand(ctx context.Context, cmd *cli.Command) (context.Context, err var query = []byte("{}") var header = []byte("{}") - return context.WithValue(ctx, apiCommandKey(cmd.Name), &apiCommandContext{client, body, query, header}), nil -} - -func getAPICommandContext(ctx context.Context, cmd *cli.Command) *apiCommandContext { - return ctx.Value(apiCommandKey(cmd.Name)).(*apiCommandContext) -} - -func getAPIFlagAction[T any](kind string, path string) func(context.Context, *cli.Command, T) error { - return func(ctx context.Context, cmd *cli.Command, value T) (err error) { - commandContext := getAPICommandContext(ctx, cmd) - var dest *[]byte - switch kind { - case "body": - dest = &commandContext.body - case "query": - dest = &commandContext.query - case "header": - dest = &commandContext.header - } - *dest, err = jsonSet(*dest, path, value) - return err - } -} - -func getAPIFlagFileAction(kind string, path string) func(context.Context, *cli.Command, string) error { - return func(ctx context.Context, cmd *cli.Command, filePath string) (err error) { - value, err := os.ReadFile(filePath) - if err != nil { - return err - } - commandContext := getAPICommandContext(ctx, cmd) - var dest *[]byte - switch kind { - case "body": - dest = &commandContext.body - case "query": - dest = &commandContext.query - case "header": - dest = &commandContext.header - } - *dest, err = jsonSet(*dest, path, value) - return err + // Apply JSON flag mutations + body, query, header, err := jsonflag.Apply(body, query, header) + if err != nil { + log.Fatal(err) } -} -func getAPIFlagActionWithValue[T any](kind string, path string, value interface{}) func(context.Context, *cli.Command, T) error { - return func(ctx context.Context, cmd *cli.Command, unusedValue T) (err error) { - commandContext := getAPICommandContext(ctx, cmd) - var dest *[]byte - switch kind { - case "body": - dest = &commandContext.body - case "query": - dest = &commandContext.query - case "header": - dest = &commandContext.header - } - *dest, err = jsonSet(*dest, path, value) - return err - } + return &apiCommandContext{client, body, query, header} } func serializeQuery(params []byte) url.Values { diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go new file mode 100644 index 0000000..b5fdbce --- /dev/null +++ b/pkg/jsonflag/json_flag.go @@ -0,0 +1,306 @@ +package jsonflag + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/urfave/cli/v3" +) + +type JSONConfig struct { + Kind MutationKind + Path string + // For boolean flags that set a specific value when present + SetValue interface{} +} + +type JsonValueCreator[T any] struct{} + +func (c JsonValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { + *dest = val + return &jsonValue[T]{ + destination: dest, + config: config, + } +} + +func (c JsonValueCreator[T]) ToString(val T) string { + switch v := any(val).(type) { + case string: + if v == "" { + return v + } + return fmt.Sprintf("%q", v) + case bool: + return strconv.FormatBool(v) + case int: + return strconv.Itoa(v) + case float64: + return strconv.FormatFloat(v, 'g', -1, 64) + case time.Time: + return v.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", v) + } +} + +type jsonValue[T any] struct { + destination *T + config JSONConfig +} + +func (v *jsonValue[T]) Set(val string) error { + var parsed T + var err error + + // If SetValue is configured, use that value instead of parsing the input + if v.config.SetValue != nil { + // For boolean flags with SetValue, register the configured value + if _, isBool := any(parsed).(bool); isBool { + globalRegistry.Register(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(true).(T) // Set the flag itself to true + return nil + } + // For any flags with SetValue, register the configured value + if _, isAny := any(parsed).(interface{}); isAny { + globalRegistry.Register(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(v.config.SetValue).(T) + return nil + } + } + + switch any(parsed).(type) { + case string: + parsed = any(val).(T) + case bool: + boolVal, parseErr := strconv.ParseBool(val) + if parseErr != nil { + return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) + } + parsed = any(boolVal).(T) + case int: + intVal, parseErr := strconv.Atoi(val) + if parseErr != nil { + return fmt.Errorf("invalid integer value %q: %w", val, parseErr) + } + parsed = any(intVal).(T) + case float64: + floatVal, parseErr := strconv.ParseFloat(val, 64) + if parseErr != nil { + return fmt.Errorf("invalid float value %q: %w", val, parseErr) + } + parsed = any(floatVal).(T) + case time.Time: + // Try common datetime formats + formats := []string{ + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + "2006-01-02", + "15:04:05", + "15:04", + } + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) + } + parsed = any(timeVal).(T) + case interface{}: + // For interface{}, store the string value directly + parsed = any(val).(T) + default: + return fmt.Errorf("unsupported type for JSON flag") + } + + *v.destination = parsed + globalRegistry.Register(v.config.Kind, v.config.Path, parsed) + return err +} + +func (v *jsonValue[T]) Get() any { + if v.destination != nil { + return *v.destination + } + var zero T + return zero +} + +func (v *jsonValue[T]) String() string { + if v.destination != nil { + switch val := any(*v.destination).(type) { + case string: + return val + case bool: + return strconv.FormatBool(val) + case int: + return strconv.Itoa(val) + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case time.Time: + return val.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", val) + } + } + var zero T + switch any(zero).(type) { + case string: + return "" + case bool: + return "false" + case int: + return "0" + case float64: + return "0" + case time.Time: + return "" + default: + return fmt.Sprintf("%v", zero) + } +} + +func (v *jsonValue[T]) IsBoolFlag() bool { + return v.config.SetValue != nil +} + +// JsonDateValueCreator is a specialized creator for date-only values +type JsonDateValueCreator struct{} + +func (c JsonDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { + *dest = val + return &jsonDateValue{ + destination: dest, + config: config, + } +} + +func (c JsonDateValueCreator) ToString(val time.Time) string { + return val.Format("2006-01-02") +} + +type jsonDateValue struct { + destination *time.Time + config JSONConfig +} + +func (v *jsonDateValue) Set(val string) error { + // Try date-only formats first, then fall back to datetime formats + formats := []string{ + "2006-01-02", + "01/02/2006", + "Jan 2, 2006", + "January 2, 2006", + "2-Jan-2006", + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + } + + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid date value %q: %w", val, parseErr) + } + + *v.destination = timeVal + globalRegistry.Register(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) + return nil +} + +func (v *jsonDateValue) Get() any { + if v.destination != nil { + return *v.destination + } + return time.Time{} +} + +func (v *jsonDateValue) String() string { + if v.destination != nil { + return v.destination.Format("2006-01-02") + } + return "" +} + +func (v *jsonDateValue) IsBoolFlag() bool { + return false +} + +type JSONStringFlag = cli.FlagBase[string, JSONConfig, JsonValueCreator[string]] +type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JsonValueCreator[bool]] +type JSONIntFlag = cli.FlagBase[int, JSONConfig, JsonValueCreator[int]] +type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JsonValueCreator[float64]] +type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JsonValueCreator[time.Time]] +type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JsonDateValueCreator] +type JSONAnyFlag = cli.FlagBase[interface{}, JSONConfig, JsonValueCreator[interface{}]] + +// JsonFileValueCreator handles file-based flags that read content and register with mutations +type JsonFileValueCreator struct{} + +func (c JsonFileValueCreator) Create(val string, dest *string, config JSONConfig) cli.Value { + *dest = val + return &jsonFileValue{ + destination: dest, + config: config, + } +} + +func (c JsonFileValueCreator) ToString(val string) string { + return val +} + +type jsonFileValue struct { + destination *string + config JSONConfig +} + +func (v *jsonFileValue) Set(filePath string) error { + // Read the file content + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %q: %w", filePath, err) + } + + // Store the file path in the destination + *v.destination = filePath + + // Register the file content with the global registry + globalRegistry.Register(v.config.Kind, v.config.Path, string(content)) + return nil +} + +func (v *jsonFileValue) Get() any { + if v.destination != nil { + return *v.destination + } + return "" +} + +func (v *jsonFileValue) String() string { + if v.destination != nil { + return *v.destination + } + return "" +} + +func (v *jsonFileValue) IsBoolFlag() bool { + return false +} + +type JSONFileFlag = cli.FlagBase[string, JSONConfig, JsonFileValueCreator] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go new file mode 100644 index 0000000..b5c525c --- /dev/null +++ b/pkg/jsonflag/mutation.go @@ -0,0 +1,99 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type MutationKind string + +const ( + Body MutationKind = "body" + Query MutationKind = "query" + Header MutationKind = "header" +) + +type Mutation struct { + Kind MutationKind + Path string + Value interface{} +} + +type registry struct { + mutations []Mutation +} + +var globalRegistry = ®istry{} + +func (r *registry) Register(kind MutationKind, path string, value interface{}) { + r.mutations = append(r.mutations, Mutation{ + Kind: kind, + Path: path, + Value: value, + }) +} + +func (r *registry) ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { + var err error + + for _, mutation := range r.mutations { + switch mutation.Kind { + case Body: + body, err = jsonSet(body, mutation.Path, mutation.Value) + case Query: + query, err = jsonSet(query, mutation.Path, mutation.Value) + case Header: + header, err = jsonSet(header, mutation.Path, mutation.Value) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) + } + } + + return body, query, header, nil +} + +func (r *registry) Clear() { + r.mutations = nil +} + +func (r *registry) List() []Mutation { + result := make([]Mutation, len(r.mutations)) + copy(result, r.mutations) + return result +} + +func Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { + body, query, header, err := globalRegistry.ApplyMutations(body, query, header) + globalRegistry.Clear() + return body, query, header, err +} + +func Clear() { + globalRegistry.Clear() +} + +func Register(kind MutationKind, path string, value interface{}) { + globalRegistry.Register(kind, path, value) +} + +func jsonSet(json []byte, path string, value interface{}) ([]byte, error) { + keys := strings.Split(path, ".") + path = "" + for i := 0; i < len(keys); i++ { + key := keys[i] + if key == "#" { + key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) + } + + if len(path) > 0 { + path += "." + } + path += key + } + return sjson.SetBytes(json, path, value) +} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go new file mode 100644 index 0000000..1ba78e1 --- /dev/null +++ b/pkg/jsonflag/mutation_test.go @@ -0,0 +1,39 @@ +package jsonflag + +import ( + "testing" + + "github.com/urfave/cli/v3" +) + +func TestApply(t *testing.T) { + Clear() + + globalRegistry.Register(Body, "name", "test") + globalRegistry.Register(Query, "page", 1) + globalRegistry.Register(Header, "authorization", "Bearer token") + + body, query, header, err := globalRegistry.ApplyMutations( + []byte(`{}`), + []byte(`{}`), + []byte(`{}`), + ) + + if err != nil { + t.Fatalf("Failed to apply mutations: %v", err) + } + + expectedBody := `{"name":"test"}` + expectedQuery := `{"page":1}` + expectedHeader := `{"authorization":"Bearer token"}` + + if string(body) != expectedBody { + t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) + } + if string(query) != expectedQuery { + t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) + } + if string(header) != expectedHeader { + t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) + } +}