• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

astronomer / astro-cli / d8c087d9-3e34-4a26-9912-4e9425408752

04 Feb 2026 08:31PM UTC coverage: 32.799% (+0.005%) from 32.794%
d8c087d9-3e34-4a26-9912-4e9425408752

push

circleci

jeremybeard
Fix linting

18 of 95 new or added lines in 9 files covered. (18.95%)

5 existing lines in 1 file now uncovered.

21211 of 64670 relevant lines covered (32.8%)

8.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cmd/api/describe.go
1
package api
2

3
import (
4
        "fmt"
5
        "io"
6
        "sort"
7
        "strings"
8

9
        "github.com/astronomer/astro-cli/pkg/openapi"
10
        "github.com/fatih/color"
11
        "github.com/spf13/cobra"
12
)
13

14
const (
15
        schemaTypeObject  = "object"
16
        separatorWidth    = 60
17
        indentIncrement   = 4
18
        maxSchemaIndent   = 10
19
        baseResponseDepth = 4
20
)
21

22
// DescribeOptions holds options for the describe command.
23
type DescribeOptions struct {
24
        Out       io.Writer
25
        specCache *openapi.Cache
26
        Endpoint  string
27
        Method    string
28
        Refresh   bool
29
}
30

31
// NewDescribeCmd creates the 'astro api cloud describe' command.
32
func NewDescribeCmd(out io.Writer, specCache *openapi.Cache) *cobra.Command {
×
33
        opts := &DescribeOptions{
×
34
                Out:       out,
×
35
                specCache: specCache,
×
36
        }
×
37

×
38
        cmd := &cobra.Command{
×
39
                Use:   "describe <endpoint>",
×
40
                Short: "Describe an API endpoint's request and response schema",
×
41
                Long: `Show detailed information about an API endpoint, including:
×
42
- Path and query parameters
×
43
- Request body schema (for POST/PUT/PATCH)
×
44
- Response schema
×
45

×
46
The endpoint can be specified as a path (e.g., /organizations/{organizationId}/deployments)
×
47
or as an operation ID (e.g., CreateDeployment).`,
×
48
                Example: `  # Describe an endpoint by path
×
49
  astro api cloud describe /organizations/{organizationId}/deployments
×
50

×
51
  # Describe a POST endpoint specifically
×
52
  astro api cloud describe /organizations/{organizationId}/deployments -X POST
×
53

×
54
  # Describe by operation ID
×
55
  astro api cloud describe CreateDeployment`,
×
56
                Args: cobra.ExactArgs(1),
×
57
                RunE: func(cmd *cobra.Command, args []string) error {
×
58
                        opts.Endpoint = args[0]
×
59
                        return runDescribe(opts)
×
60
                },
×
61
        }
62

63
        cmd.Flags().StringVarP(&opts.Method, "method", "X", "", "HTTP method (GET, POST, PUT, PATCH, DELETE)")
×
64
        cmd.Flags().BoolVar(&opts.Refresh, "refresh", false, "Force refresh of the OpenAPI specification cache")
×
65

×
66
        return cmd
×
67
}
68

69
// runDescribe executes the describe command.
70
func runDescribe(opts *DescribeOptions) error {
×
71
        // Load OpenAPI spec
×
72
        if err := opts.specCache.Load(opts.Refresh); err != nil {
×
73
                return fmt.Errorf("loading OpenAPI spec: %w", err)
×
74
        }
×
75

76
        spec := opts.specCache.GetSpec()
×
77
        endpoints := opts.specCache.GetEndpoints()
×
78
        if len(endpoints) == 0 {
×
79
                return fmt.Errorf("no endpoints found in API specification")
×
80
        }
×
81

82
        // Find the endpoint
83
        var matches []openapi.Endpoint
×
84

×
85
        // First, try to find by operation ID
×
NEW
86
        for i := range endpoints {
×
NEW
87
                if strings.EqualFold(endpoints[i].OperationID, opts.Endpoint) {
×
NEW
88
                        matches = append(matches, endpoints[i])
×
UNCOV
89
                }
×
90
        }
91

92
        // If no match by operation ID, try by path
93
        if len(matches) == 0 {
×
94
                path := opts.Endpoint
×
95
                if !strings.HasPrefix(path, "/") {
×
96
                        path = "/" + path
×
97
                }
×
98

NEW
99
                for i := range endpoints {
×
NEW
100
                        if endpoints[i].Path == path {
×
NEW
101
                                matches = append(matches, endpoints[i])
×
UNCOV
102
                        }
×
103
                }
104
        }
105

106
        // Filter by method if specified
107
        if opts.Method != "" && len(matches) > 0 {
×
108
                method := strings.ToUpper(opts.Method)
×
109
                var filtered []openapi.Endpoint
×
NEW
110
                for i := range matches {
×
NEW
111
                        if matches[i].Method == method {
×
NEW
112
                                filtered = append(filtered, matches[i])
×
UNCOV
113
                        }
×
114
                }
115
                matches = filtered
×
116
        }
117

118
        if len(matches) == 0 {
×
119
                return fmt.Errorf("no endpoint found matching '%s'", opts.Endpoint)
×
120
        }
×
121

122
        // If multiple matches and no method specified, show all
123
        resolver := openapi.NewSchemaResolver(spec)
×
124

×
NEW
125
        for i := range matches {
×
126
                if i > 0 {
×
NEW
127
                        fmt.Fprintln(opts.Out, "\n"+strings.Repeat("─", separatorWidth)+"\n")
×
128
                }
×
NEW
129
                printEndpointDetails(opts.Out, &matches[i], resolver)
×
130
        }
131

132
        return nil
×
133
}
134

135
// printEndpointDetails prints detailed information about an endpoint.
NEW
136
func printEndpointDetails(out io.Writer, ep *openapi.Endpoint, resolver *openapi.SchemaResolver) {
×
137
        // Header
×
138
        method := colorizeMethod(ep.Method)
×
139
        fmt.Fprintf(out, "%s %s\n", method, ep.Path)
×
140

×
141
        if ep.Deprecated {
×
142
                fmt.Fprintf(out, "%s\n", color.YellowString("âš  DEPRECATED"))
×
143
        }
×
144

145
        if ep.OperationID != "" {
×
146
                fmt.Fprintf(out, "Operation ID: %s\n", color.CyanString(ep.OperationID))
×
147
        }
×
148

149
        if ep.Summary != "" {
×
150
                fmt.Fprintf(out, "\n%s\n", ep.Summary)
×
151
        }
×
152

153
        if ep.Description != "" && ep.Description != ep.Summary {
×
154
                fmt.Fprintf(out, "\n%s\n", ep.Description)
×
155
        }
×
156

157
        if len(ep.Tags) > 0 {
×
158
                fmt.Fprintf(out, "\nTags: %s\n", strings.Join(ep.Tags, ", "))
×
159
        }
×
160

161
        // Parameters
162
        printParameters(out, ep.Parameters)
×
163

×
164
        // Request Body
×
165
        if ep.RequestBody != nil {
×
166
                printRequestBody(out, ep.RequestBody, resolver)
×
167
        }
×
168

169
        // Responses
170
        printResponses(out, ep.Responses, resolver)
×
171
}
172

173
// printParameters prints parameter information.
174
func printParameters(out io.Writer, params []openapi.Parameter) {
×
175
        if len(params) == 0 {
×
176
                return
×
177
        }
×
178

179
        // Group by location
180
        pathParams := filterParams(params, "path")
×
181
        queryParams := filterParams(params, "query")
×
182
        headerParams := filterParams(params, "header")
×
183

×
184
        if len(pathParams) > 0 {
×
185
                fmt.Fprintf(out, "\n%s\n", color.New(color.Bold).Sprint("Path Parameters:"))
×
186
                for _, p := range pathParams {
×
187
                        printParam(out, p)
×
188
                }
×
189
        }
190

191
        if len(queryParams) > 0 {
×
192
                fmt.Fprintf(out, "\n%s\n", color.New(color.Bold).Sprint("Query Parameters:"))
×
193
                for _, p := range queryParams {
×
194
                        printParam(out, p)
×
195
                }
×
196
        }
197

198
        if len(headerParams) > 0 {
×
199
                fmt.Fprintf(out, "\n%s\n", color.New(color.Bold).Sprint("Header Parameters:"))
×
200
                for _, p := range headerParams {
×
201
                        printParam(out, p)
×
202
                }
×
203
        }
204
}
205

206
// filterParams filters parameters by location.
207
func filterParams(params []openapi.Parameter, in string) []openapi.Parameter {
×
208
        var result []openapi.Parameter
×
209
        for _, p := range params {
×
210
                if p.In == in {
×
211
                        result = append(result, p)
×
212
                }
×
213
        }
214
        return result
×
215
}
216

217
// printParam prints a single parameter.
218
func printParam(out io.Writer, p openapi.Parameter) {
×
219
        required := ""
×
220
        if p.Required {
×
221
                required = color.RedString(" (required)")
×
222
        }
×
223

224
        typeStr := "string"
×
225
        if p.Schema != nil && p.Schema.Type != "" {
×
226
                typeStr = p.Schema.Type
×
227
                if p.Schema.Format != "" {
×
228
                        typeStr += " (" + p.Schema.Format + ")"
×
229
                }
×
230
                if p.Schema.Type == "array" && p.Schema.Items != nil {
×
231
                        typeStr = "array of " + p.Schema.Items.Type
×
232
                }
×
233
        }
234

235
        fmt.Fprintf(out, "  %s%s  %s\n", color.GreenString(p.Name), required, color.HiBlackString(typeStr))
×
236
        if p.Description != "" {
×
237
                fmt.Fprintf(out, "      %s\n", p.Description)
×
238
        }
×
239
        if p.Schema != nil {
×
240
                if p.Schema.Default != nil {
×
241
                        fmt.Fprintf(out, "      Default: %v\n", p.Schema.Default)
×
242
                }
×
243
                if len(p.Schema.Enum) > 0 {
×
244
                        fmt.Fprintf(out, "      Enum: %v\n", p.Schema.Enum)
×
245
                }
×
246
        }
247
}
248

249
// printRequestBody prints request body schema.
250
func printRequestBody(out io.Writer, body *openapi.RequestBody, resolver *openapi.SchemaResolver) {
×
251
        fmt.Fprintf(out, "\n%s", color.New(color.Bold).Sprint("Request Body"))
×
252
        if body.Required {
×
253
                fmt.Fprintf(out, " %s", color.RedString("(required)"))
×
254
        }
×
255
        fmt.Fprintln(out, ":")
×
256

×
257
        if body.Description != "" {
×
258
                fmt.Fprintf(out, "  %s\n", body.Description)
×
259
        }
×
260

261
        // Get the JSON schema
262
        if body.Content != nil {
×
263
                if mt, ok := body.Content["application/json"]; ok && mt.Schema != nil {
×
264
                        schema, refName := resolver.ResolveSchema(mt.Schema)
×
265
                        if refName != "" {
×
266
                                fmt.Fprintf(out, "  Schema: %s\n", color.CyanString(refName))
×
267
                        }
×
268
                        if schema != nil {
×
269
                                printRequestSchema(out, schema, resolver, 2, make(map[string]bool))
×
270
                        }
×
271
                }
272
        }
273
}
274

275
// printRequestSchema prints a schema for request bodies, handling oneOf/anyOf/allOf.
276
//
277
//nolint:gocognit,gocyclo // Complex but necessary for comprehensive schema display
278
func printRequestSchema(out io.Writer, schema *openapi.Schema, resolver *openapi.SchemaResolver, indent int, visited map[string]bool) {
×
279
        if schema == nil {
×
280
                return
×
281
        }
×
282

283
        prefix := strings.Repeat(" ", indent)
×
284

×
285
        // Handle $ref first
×
286
        if schema.Ref != "" {
×
287
                resolved, refName := resolver.ResolveSchema(schema)
×
288
                if resolved != nil {
×
289
                        if visited[refName] {
×
290
                                fmt.Fprintf(out, "%s(see %s above)\n", prefix, refName)
×
291
                                return
×
292
                        }
×
293
                        visited[refName] = true
×
294
                        printRequestSchema(out, resolved, resolver, indent, visited)
×
295
                }
296
                return
×
297
        }
298

299
        // Handle oneOf - show all options
300
        if len(schema.OneOf) > 0 {
×
301
                fmt.Fprintf(out, "%s%s\n", prefix, color.HiBlackString("One of the following:"))
×
NEW
302
                for i := range schema.OneOf {
×
NEW
303
                        resolved, refName := resolver.ResolveSchema(&schema.OneOf[i])
×
304
                        if refName != "" {
×
305
                                fmt.Fprintf(out, "\n%s%s %s\n", prefix, color.CyanString("Option %d:", i+1), refName)
×
306
                        } else {
×
307
                                fmt.Fprintf(out, "\n%s%s\n", prefix, color.CyanString("Option %d:", i+1))
×
308
                        }
×
309
                        if resolved != nil {
×
310
                                if visited[refName] {
×
311
                                        fmt.Fprintf(out, "%s  (see %s above)\n", prefix, refName)
×
312
                                } else {
×
313
                                        if refName != "" {
×
314
                                                visited[refName] = true
×
315
                                        }
×
316
                                        printRequestSchema(out, resolved, resolver, indent+2, visited)
×
317
                                }
318
                        }
319
                }
320
                return
×
321
        }
322

323
        // Handle anyOf - show all options
324
        if len(schema.AnyOf) > 0 {
×
325
                fmt.Fprintf(out, "%s%s\n", prefix, color.HiBlackString("Any of the following:"))
×
NEW
326
                for i := range schema.AnyOf {
×
NEW
327
                        resolved, refName := resolver.ResolveSchema(&schema.AnyOf[i])
×
328
                        if refName != "" {
×
329
                                fmt.Fprintf(out, "\n%s%s %s\n", prefix, color.CyanString("Option %d:", i+1), refName)
×
330
                        } else {
×
331
                                fmt.Fprintf(out, "\n%s%s\n", prefix, color.CyanString("Option %d:", i+1))
×
332
                        }
×
333
                        if resolved != nil {
×
334
                                if refName != "" {
×
335
                                        visited[refName] = true
×
336
                                }
×
337
                                printRequestSchema(out, resolved, resolver, indent+2, visited)
×
338
                        }
339
                }
340
                return
×
341
        }
342

343
        // Handle allOf - merge all schemas
344
        if len(schema.AllOf) > 0 {
×
345
                fmt.Fprintf(out, "%s%s\n", prefix, color.HiBlackString("All of the following:"))
×
NEW
346
                for i := range schema.AllOf {
×
NEW
347
                        resolved, refName := resolver.ResolveSchema(&schema.AllOf[i])
×
348
                        if resolved != nil {
×
349
                                if refName != "" {
×
350
                                        visited[refName] = true
×
351
                                }
×
352
                                printRequestSchema(out, resolved, resolver, indent, visited)
×
353
                        }
354
                }
355
                return
×
356
        }
357

358
        // Print properties
359
        if len(schema.Properties) > 0 {
×
360
                propNames := make([]string, 0, len(schema.Properties))
×
361
                for name := range schema.Properties {
×
362
                        propNames = append(propNames, name)
×
363
                }
×
364
                sort.Strings(propNames)
×
365

×
366
                for _, name := range propNames {
×
367
                        prop := schema.Properties[name]
×
368

×
369
                        // Skip read-only properties in request schemas
×
370
                        if prop.ReadOnly {
×
371
                                continue
×
372
                        }
373

374
                        required := ""
×
375
                        if openapi.IsRequired(name, schema.Required) {
×
376
                                required = color.RedString("*")
×
377
                        }
×
378

379
                        // Resolve the property schema if it's a $ref
380
                        propSchema := &prop
×
381
                        refName := ""
×
382
                        if prop.Ref != "" {
×
383
                                propSchema, refName = resolver.ResolveSchema(&prop)
×
384
                                if propSchema == nil {
×
385
                                        propSchema = &prop
×
386
                                }
×
387
                        }
388

389
                        typeStr := getTypeString(propSchema, refName)
×
390
                        fmt.Fprintf(out, "%s%s%s  %s\n", prefix, color.GreenString(name), required, color.HiBlackString(typeStr))
×
391

×
392
                        if propSchema.Description != "" {
×
393
                                fmt.Fprintf(out, "%s    %s\n", prefix, propSchema.Description)
×
394
                        }
×
395

396
                        if propSchema.Example != nil {
×
397
                                fmt.Fprintf(out, "%s    Example: %v\n", prefix, propSchema.Example)
×
398
                        }
×
399

400
                        if len(propSchema.Enum) > 0 {
×
401
                                fmt.Fprintf(out, "%s    Enum: %v\n", prefix, propSchema.Enum)
×
402
                        }
×
403

404
                        if propSchema.Default != nil {
×
405
                                fmt.Fprintf(out, "%s    Default: %v\n", prefix, propSchema.Default)
×
406
                        }
×
407

408
                        // Show nested object properties (but limit depth)
NEW
409
                        if indent < maxSchemaIndent && propSchema.Type == schemaTypeObject && len(propSchema.Properties) > 0 {
×
NEW
410
                                printRequestSchema(out, propSchema, resolver, indent+indentIncrement, visited)
×
UNCOV
411
                        }
×
412

413
                        // Handle nested oneOf/anyOf/allOf in properties
414
                        if len(propSchema.OneOf) > 0 || len(propSchema.AnyOf) > 0 || len(propSchema.AllOf) > 0 {
×
NEW
415
                                printRequestSchema(out, propSchema, resolver, indent+indentIncrement, visited)
×
416
                        }
×
417
                }
418
        }
419
}
420

421
// printResponses prints response information.
422
func printResponses(out io.Writer, responses map[string]openapi.Response, resolver *openapi.SchemaResolver) {
×
423
        if len(responses) == 0 {
×
424
                return
×
425
        }
×
426

427
        fmt.Fprintf(out, "\n%s\n", color.New(color.Bold).Sprint("Responses:"))
×
428

×
429
        // Sort response codes
×
430
        codes := make([]string, 0, len(responses))
×
431
        for code := range responses {
×
432
                codes = append(codes, code)
×
433
        }
×
434
        sort.Strings(codes)
×
435

×
436
        for _, code := range codes {
×
437
                resp := responses[code]
×
438
                codeColor := color.GreenString
×
439
                if code >= "400" {
×
440
                        codeColor = color.RedString
×
441
                } else if code >= "300" {
×
442
                        codeColor = color.YellowString
×
443
                }
×
444

445
                fmt.Fprintf(out, "  %s: %s\n", codeColor(code), resp.Description)
×
446

×
447
                // Show success response schema (2xx)
×
448
                if code >= "200" && code < "300" && resp.Content != nil {
×
449
                        if mt, ok := resp.Content["application/json"]; ok && mt.Schema != nil {
×
450
                                schema, refName := resolver.ResolveSchema(mt.Schema)
×
451
                                if refName != "" {
×
452
                                        fmt.Fprintf(out, "    Schema: %s\n", color.CyanString(refName))
×
453
                                }
×
454
                                if schema != nil && len(schema.Properties) > 0 {
×
NEW
455
                                        printSchema(out, schema, resolver, baseResponseDepth, make(map[string]bool))
×
456
                                }
×
457
                        }
458
                }
459
        }
460
}
461

462
// printSchema prints a schema with proper indentation.
463
//
464
//nolint:gocognit // Complex but necessary for comprehensive schema display
465
func printSchema(out io.Writer, schema *openapi.Schema, resolver *openapi.SchemaResolver, indent int, visited map[string]bool) {
×
466
        if schema == nil {
×
467
                return
×
468
        }
×
469

470
        prefix := strings.Repeat(" ", indent)
×
471

×
472
        // Handle $ref
×
473
        if schema.Ref != "" {
×
474
                resolved, refName := resolver.ResolveSchema(schema)
×
475
                if resolved != nil {
×
476
                        // Prevent infinite recursion
×
477
                        if visited[refName] {
×
478
                                fmt.Fprintf(out, "%s(see %s above)\n", prefix, refName)
×
479
                                return
×
480
                        }
×
481
                        visited[refName] = true
×
482
                        printSchema(out, resolved, resolver, indent, visited)
×
483
                }
484
                return
×
485
        }
486

487
        // Print properties
488
        if len(schema.Properties) > 0 {
×
489
                // Sort property names for consistent output
×
490
                propNames := make([]string, 0, len(schema.Properties))
×
491
                for name := range schema.Properties {
×
492
                        propNames = append(propNames, name)
×
493
                }
×
494
                sort.Strings(propNames)
×
495

×
496
                for _, name := range propNames {
×
497
                        prop := schema.Properties[name]
×
498

×
499
                        // Skip read-only properties in request schemas
×
500
                        if prop.ReadOnly {
×
501
                                continue
×
502
                        }
503

504
                        required := ""
×
505
                        if openapi.IsRequired(name, schema.Required) {
×
506
                                required = color.RedString("*")
×
507
                        }
×
508

509
                        // Resolve the property schema if it's a $ref
510
                        propSchema := &prop
×
511
                        refName := ""
×
512
                        if prop.Ref != "" {
×
513
                                propSchema, refName = resolver.ResolveSchema(&prop)
×
514
                                if propSchema == nil {
×
515
                                        propSchema = &prop
×
516
                                }
×
517
                        }
518

519
                        typeStr := getTypeString(propSchema, refName)
×
520
                        fmt.Fprintf(out, "%s%s%s  %s\n", prefix, color.GreenString(name), required, color.HiBlackString(typeStr))
×
521

×
522
                        if propSchema.Description != "" {
×
523
                                fmt.Fprintf(out, "%s    %s\n", prefix, propSchema.Description)
×
524
                        }
×
525

526
                        if propSchema.Example != nil {
×
527
                                fmt.Fprintf(out, "%s    Example: %v\n", prefix, propSchema.Example)
×
528
                        }
×
529

530
                        if len(propSchema.Enum) > 0 {
×
531
                                fmt.Fprintf(out, "%s    Enum: %v\n", prefix, propSchema.Enum)
×
532
                        }
×
533

534
                        // Show nested object properties (but limit depth)
NEW
535
                        if indent < 8 && propSchema.Type == schemaTypeObject && len(propSchema.Properties) > 0 {
×
NEW
536
                                printSchema(out, propSchema, resolver, indent+indentIncrement, visited)
×
UNCOV
537
                        }
×
538
                }
539
        }
540
}
541

542
// getTypeString returns a human-readable type string for a schema.
543
func getTypeString(schema *openapi.Schema, refName string) string {
×
544
        if schema == nil {
×
545
                return "any"
×
546
        }
×
547

548
        if refName != "" {
×
549
                return refName
×
550
        }
×
551

552
        typeStr := schema.Type
×
553
        if typeStr == "" {
×
554
                typeStr = "object"
×
555
        }
×
556

557
        if schema.Format != "" {
×
558
                typeStr += " (" + schema.Format + ")"
×
559
        }
×
560

561
        if schema.Type == "array" && schema.Items != nil {
×
562
                itemType := schema.Items.Type
×
563
                if schema.Items.Ref != "" {
×
564
                        // Extract ref name
×
565
                        parts := strings.Split(schema.Items.Ref, "/")
×
566
                        itemType = parts[len(parts)-1]
×
567
                }
×
568
                if itemType == "" {
×
569
                        itemType = "object"
×
570
                }
×
571
                typeStr = "array of " + itemType
×
572
        }
573

574
        return typeStr
×
575
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc