|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + |
| 9 | + "github.com/libops/sitectl-drupal/drupal" |
| 10 | + "github.com/spf13/cobra" |
| 11 | +) |
| 12 | + |
| 13 | +// clientKey is the context key for the Drupal client |
| 14 | +type clientKey struct{} |
| 15 | + |
| 16 | +var bundleConfig string |
| 17 | + |
| 18 | +// nodeCmd represents the node command group |
| 19 | +var nodeCmd = &cobra.Command{ |
| 20 | + Use: "node", |
| 21 | + Short: "Drupal node operations", |
| 22 | + Long: `Commands for interacting with Drupal nodes via the JSON API. |
| 23 | +
|
| 24 | +Use --bundle-config to load bundle definitions from a Drupal config sync export |
| 25 | +or generated YAML file. This enables field validation and type information.`, |
| 26 | + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { |
| 27 | + var opts []drupal.ClientOption |
| 28 | + |
| 29 | + // Load bundle config if provided |
| 30 | + if bundleConfig != "" { |
| 31 | + opts = append(opts, drupal.WithBundlesFromPath(bundleConfig)) |
| 32 | + } |
| 33 | + |
| 34 | + // Load auth from environment |
| 35 | + opts = append(opts, drupal.WithBasicAuthFromEnv("DRUPAL_USERNAME", "DRUPAL_PASSWORD")) |
| 36 | + |
| 37 | + client := drupal.NewClient(opts...) |
| 38 | + |
| 39 | + // Store client in context for subcommands |
| 40 | + ctx := context.WithValue(cmd.Context(), clientKey{}, client) |
| 41 | + cmd.SetContext(ctx) |
| 42 | + |
| 43 | + return nil |
| 44 | + }, |
| 45 | +} |
| 46 | + |
| 47 | +func init() { |
| 48 | + // Register persistent flag for bundle configuration |
| 49 | + nodeCmd.PersistentFlags().StringVar(&bundleConfig, "bundle-config", "", |
| 50 | + "Path to bundle configuration YAML (from Drupal config sync or generated)") |
| 51 | + |
| 52 | + // Add subcommands |
| 53 | + nodeCmd.AddCommand(nodeFetchCmd) |
| 54 | + nodeCmd.AddCommand(nodeValidateCmd) |
| 55 | + nodeCmd.AddCommand(nodeBundlesCmd) |
| 56 | + nodeCmd.AddCommand(nodeCacheClearCmd) |
| 57 | +} |
| 58 | + |
| 59 | +// getClient retrieves the Drupal client from the command context |
| 60 | +func getClient(cmd *cobra.Command) *drupal.Client { |
| 61 | + return cmd.Context().Value(clientKey{}).(*drupal.Client) |
| 62 | +} |
| 63 | + |
| 64 | +// nodeFetchCmd fetches and displays a node |
| 65 | +var nodeFetchCmd = &cobra.Command{ |
| 66 | + Use: "fetch <url>", |
| 67 | + Short: "Fetch a node from the Drupal JSON API", |
| 68 | + Long: `Fetches a node from the Drupal JSON API and displays it as JSON. |
| 69 | +
|
| 70 | +The URL should be a full URL to the node JSON endpoint, e.g.: |
| 71 | + https://example.com/node/123?_format=json |
| 72 | +
|
| 73 | +If --bundle-config is provided, the node will be validated against its bundle schema.`, |
| 74 | + Args: cobra.ExactArgs(1), |
| 75 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 76 | + url := args[0] |
| 77 | + client := getClient(cmd) |
| 78 | + |
| 79 | + node, err := client.FetchNode(url) |
| 80 | + if err != nil { |
| 81 | + return fmt.Errorf("fetching node: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + // Validate if registry has bundles loaded |
| 85 | + if errors := node.Validate(); len(errors) > 0 { |
| 86 | + fmt.Fprintf(os.Stderr, "Warning: validation errors for bundle %q:\n", node.Bundle()) |
| 87 | + for _, e := range errors { |
| 88 | + fmt.Fprintf(os.Stderr, " - %s\n", e) |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + // Output as JSON |
| 93 | + enc := json.NewEncoder(os.Stdout) |
| 94 | + enc.SetIndent("", " ") |
| 95 | + return enc.Encode(node) |
| 96 | + }, |
| 97 | +} |
| 98 | + |
| 99 | +// nodeValidateCmd validates a node against bundle schema |
| 100 | +var nodeValidateCmd = &cobra.Command{ |
| 101 | + Use: "validate <url>", |
| 102 | + Short: "Validate a node against its bundle schema", |
| 103 | + Long: `Fetches a node and validates it against the bundle schema. |
| 104 | +
|
| 105 | +Requires --bundle-config to be set with bundle definitions.`, |
| 106 | + Args: cobra.ExactArgs(1), |
| 107 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 108 | + url := args[0] |
| 109 | + client := getClient(cmd) |
| 110 | + |
| 111 | + node, err := client.FetchNode(url) |
| 112 | + if err != nil { |
| 113 | + return fmt.Errorf("fetching node: %w", err) |
| 114 | + } |
| 115 | + |
| 116 | + registry := node.Registry() |
| 117 | + if registry == nil { |
| 118 | + return fmt.Errorf("no bundle definitions loaded - use --bundle-config") |
| 119 | + } |
| 120 | + |
| 121 | + def, ok := registry.GetBundle(node.Bundle()) |
| 122 | + if !ok { |
| 123 | + return fmt.Errorf("unknown bundle %q - not in registry", node.Bundle()) |
| 124 | + } |
| 125 | + |
| 126 | + fmt.Printf("Validating node (nid: %s) against bundle: %s\n", node.Nid.String(), def.Name) |
| 127 | + if def.Description != "" { |
| 128 | + fmt.Printf("Bundle description: %s\n", def.Description) |
| 129 | + } |
| 130 | + fmt.Println() |
| 131 | + |
| 132 | + errors := node.Validate() |
| 133 | + if len(errors) == 0 { |
| 134 | + fmt.Println("Node is valid") |
| 135 | + return nil |
| 136 | + } |
| 137 | + |
| 138 | + fmt.Println("Validation errors:") |
| 139 | + for _, e := range errors { |
| 140 | + fmt.Printf(" - %s\n", e) |
| 141 | + } |
| 142 | + return fmt.Errorf("validation failed with %d error(s)", len(errors)) |
| 143 | + }, |
| 144 | +} |
| 145 | + |
| 146 | +// nodeBundlesCmd lists registered bundles |
| 147 | +var nodeBundlesCmd = &cobra.Command{ |
| 148 | + Use: "bundles", |
| 149 | + Short: "List all registered bundle definitions", |
| 150 | + Long: `Lists all bundle definitions loaded from --bundle-config. |
| 151 | +
|
| 152 | +Shows bundle names, descriptions, field counts, and required fields.`, |
| 153 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 154 | + client := getClient(cmd) |
| 155 | + |
| 156 | + bundles := client.Registry.ListBundles() |
| 157 | + if len(bundles) == 0 { |
| 158 | + fmt.Println("No bundles registered") |
| 159 | + fmt.Println("Use --bundle-config to load bundle definitions") |
| 160 | + return nil |
| 161 | + } |
| 162 | + |
| 163 | + fmt.Println("Registered bundles:") |
| 164 | + for _, name := range bundles { |
| 165 | + def, _ := client.Registry.GetBundle(name) |
| 166 | + fmt.Printf("\n %s (%s)\n", def.Name, def.MachineName) |
| 167 | + if def.Description != "" { |
| 168 | + fmt.Printf(" %s\n", def.Description) |
| 169 | + } |
| 170 | + fmt.Printf(" Fields: %d\n", len(def.Fields)) |
| 171 | + |
| 172 | + // List required fields |
| 173 | + var required []string |
| 174 | + for _, f := range def.Fields { |
| 175 | + if f.Required { |
| 176 | + required = append(required, f.Name) |
| 177 | + } |
| 178 | + } |
| 179 | + if len(required) > 0 { |
| 180 | + fmt.Printf(" Required: %v\n", required) |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + return nil |
| 185 | + }, |
| 186 | +} |
| 187 | + |
| 188 | +// nodeCacheClearCmd clears the bundle definition cache |
| 189 | +var nodeCacheClearCmd = &cobra.Command{ |
| 190 | + Use: "cache-clear", |
| 191 | + Short: "Clear cached bundle definitions", |
| 192 | + Long: `Clears the bundle definition cache from ~/.sitectl/cache`, |
| 193 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 194 | + if err := drupal.ClearCache(); err != nil { |
| 195 | + return fmt.Errorf("clearing cache: %w", err) |
| 196 | + } |
| 197 | + fmt.Println("Bundle definition cache cleared") |
| 198 | + return nil |
| 199 | + }, |
| 200 | +} |
0 commit comments