diff --git a/.vscode/settings.json b/.vscode/settings.json index 7aeaf46..4ee31ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,8 @@ { - "files.associations": { - "Spacefile": "yaml" - }, - "yaml.schemas": { - "internal/spacefile/schemas/spacefile.json": [ - "Spacefile" - ] - } + "files.associations": { + "Spacefile": "yaml" + }, + "yaml.schemas": { + "internal/spacefile/schemas/spacefile.schema.json": ["Spacefile"] + } } diff --git a/cmd/validate.go b/cmd/validate.go index ca3cf47..684de6e 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -32,13 +32,11 @@ func newCmdValidate() *cobra.Command { } func validate(projectDir string) error { - shared.Logger.Printf("\n%s Validating Spacefile...", emoji.Package) + shared.Logger.Printf("\n%sValidating Spacefile...", emoji.Package) s, err := spacefile.LoadSpacefile(projectDir) if err != nil { - shared.Logger.Println(styles.Errorf("\n%s Detected some issues with your Spacefile. Please fix them before pushing your code.", emoji.ErrorExclamation)) - shared.Logger.Println() - shared.Logger.Println(err.Error()) + shared.Logger.Println(styles.Errorf("\n%s Detected some issues with your Spacefile. Please fix them before pushing your code.\n", emoji.ErrorExclamation)) return err } diff --git a/internal/api/api.go b/internal/api/api.go index 4273ffa..0e79975 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -305,19 +305,19 @@ func (c *DetaClient) PushSpacefile(r *PushSpacefileRequest) (*PushSpacefileRespo } o, err := c.request(i) - if err != nil { return nil, err } + if !(o.Status >= 200 && o.Status <= 299) { msg := o.Error.Detail - return nil, fmt.Errorf("failed to push spacefile file, %v", msg) + return nil, fmt.Errorf(msg) } var resp PushSpacefileResponse err = json.Unmarshal(o.Body, &resp) if err != nil { - return nil, fmt.Errorf("failed to push spacefile file %w", err) + return nil, fmt.Errorf("unable to read response: %w", err) } return &resp, nil diff --git a/internal/spacefile/spacefile.go b/internal/spacefile/spacefile.go index 1162c73..b6a47c1 100644 --- a/internal/spacefile/spacefile.go +++ b/internal/spacefile/spacefile.go @@ -46,198 +46,68 @@ type Spacefile struct { Micros []*shared.Micro `yaml:"micros,omitempty"` } -func extractMicro(v any, index int) (map[string]any, bool) { +type SpacefileValidationError struct { + wrapped *jsonschema.ValidationError + raw any +} + +func extractMicroName(v any, index int) (string, bool) { spacefile, ok := v.(map[string]interface{}) if !ok { - return nil, false + return "", false } micros, ok := spacefile["micros"].([]interface{}) if !ok { - return nil, false + return "", false } micro, ok := micros[index].(map[string]interface{}) if !ok { - return nil, false - } - - return micro, true -} - -func extractPresets(v any, microIndex int) (map[string]any, bool) { - micro, ok := extractMicro(v, microIndex) - if !ok { - return nil, false - } - - presets, ok := micro["presets"].(map[string]interface{}) - if !ok { - return nil, false - } - - return presets, true -} - -func extractAction(v any, microIndex int, actionIndex int) (map[string]any, bool) { - micro, ok := extractMicro(v, microIndex) - if !ok { - return nil, false - } - - actions, ok := micro["actions"].([]interface{}) - if !ok { - return nil, false - } - - action, ok := actions[actionIndex].(map[string]interface{}) - if !ok { - return nil, false - } - - return action, true -} - -func extractEnv(v any, microIndex int, envIndex int) (map[string]any, bool) { - presets, ok := extractPresets(v, microIndex) - if !ok { - return nil, false - } - - envs, ok := presets["env"].([]interface{}) - if !ok { - return nil, false - } - - env, ok := envs[envIndex].(map[string]interface{}) - if !ok { - return nil, false + return "", false } - return env, true -} - -func extractApiKey(v any, microIndex int, apiKeyIndex int) (map[string]any, bool) { - presets, ok := extractPresets(v, microIndex) - if !ok { - return nil, false - } - - apiKeys, ok := presets["api_keys"].([]interface{}) - if !ok { - return nil, false - } - - apiKey, ok := apiKeys[apiKeyIndex].(map[string]interface{}) + name, ok := micro["name"].(string) if !ok { - return nil, false + return "", false } - return apiKey, true + return name, true } -var ( - microReg = regexp.MustCompile(`\/micros\/(\d+)$`) - actionReg = regexp.MustCompile(`\/micros\/(\d+)\/actions\/(\d+)$`) - commandsReg = regexp.MustCompile(`\/micros\/(\d+)\/commands$`) - includeReg = regexp.MustCompile(`\/micros\/(\d+)\/include$`) - publicRoutesReg = regexp.MustCompile(`\/micros\/(\d+)\/public_routes$`) - presetsReg = regexp.MustCompile(`\/micros\/(\d+)\/presets$`) - envReg = regexp.MustCompile(`\/micros\/(\d+)\/presets\/env\/(\d+)$`) - apiKeyReg = regexp.MustCompile(`\/micros\/(\d+)\/presets\/api_keys\/(\d+)$`) - numberReg = regexp.MustCompile(`^\d+$`) -) - -func PrettyValidationErrors(ve *jsonschema.ValidationError, v any, prefix string) string { - // Skip the root error - if ve.KeywordLocation == "" { - return PrettyValidationErrors(ve.Causes[0], v, prefix) - } - - // If there are no causes, just print the message - if len(ve.Causes) == 0 { - message := strings.Replace(ve.Message, "additionalProperties", "unknown field", 1) - parts := strings.Split(ve.InstanceLocation, "/") - - leaf := parts[len(parts)-1] - if leaf == "" || numberReg.MatchString(leaf) { - return fmt.Sprintf("%sL %s", prefix, message) - } - - return fmt.Sprintf("%sL %s -> %s", prefix, leaf, message) - } +var microReg = regexp.MustCompile(`\/micros\/(\d+)`) - var rows []string - if matches := microReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 2 { - i, _ := strconv.Atoi(matches[1]) - micro, ok := extractMicro(v, i) - if !ok { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Micro at index "+matches[1])) +func (ve SpacefileValidationError) Error() string { + errorMsg := func(leaf *jsonschema.ValidationError) string { + matches := microReg.FindStringSubmatch(leaf.InstanceLocation) + if len(matches) == 0 { + return fmt.Sprintf("L %s: %s", leaf.InstanceLocation, leaf.Message) } - if name, ok := micro["name"].(string); ok { - rows = append(rows, fmt.Sprintf("%sL Micro '%s'", prefix, name)) - } else { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Micro at index "+matches[1])) - } - } else if matches := presetsReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 2 { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Presets")) - } else if matches := publicRoutesReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 2 { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Public Routes")) - } else if matches := commandsReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 2 { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Commands")) - } else if matches := includeReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 2 { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Include")) - } else if matches := actionReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 3 { - i, _ := strconv.Atoi(matches[1]) - j, _ := strconv.Atoi(matches[2]) - action, ok := extractAction(v, i, j) + i := matches[1] + idx, _ := strconv.Atoi(i) + name, ok := extractMicroName(ve.raw, idx) if !ok { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Action at index "+matches[2])) + return fmt.Sprintf("L %s: %s", leaf.InstanceLocation, leaf.Message) } - if name, ok := action["name"].(string); ok { - rows = append(rows, fmt.Sprintf("%sL Action '%s'", prefix, name)) - } else { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Action at index "+matches[2])) - } - } else if matches := envReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 3 { - i, _ := strconv.Atoi(matches[1]) - j, _ := strconv.Atoi(matches[2]) - env, ok := extractEnv(v, i, j) - if !ok { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Env at index "+matches[2])) - } + return fmt.Sprintf("L %s: %s", strings.Replace(leaf.InstanceLocation, i, name, 1), leaf.Message) + } - if name, ok := env["name"].(string); ok { - rows = append(rows, fmt.Sprintf("%sL Env '%s'", prefix, name)) - } else { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L Env at index "+matches[2])) - } - } else if matches := apiKeyReg.FindStringSubmatch(ve.InstanceLocation); len(matches) == 3 { - i, _ := strconv.Atoi(matches[1]) - j, _ := strconv.Atoi(matches[2]) - apiKey, ok := extractApiKey(v, i, j) - if !ok { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L API Key at index "+matches[2])) - } + queue := []*jsonschema.ValidationError{ve.wrapped} + rootErrors := []string{} + for len(queue) > 0 { + leaf := queue[0] + queue = queue[1:] - if name, ok := apiKey["name"].(string); ok { - rows = append(rows, fmt.Sprintf("%sL API Key '%s'", prefix, name)) + if len(leaf.Causes) == 0 { + rootErrors = append(rootErrors, errorMsg(leaf)) } else { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L API Key at index "+matches[2])) + queue = append(queue, leaf.Causes...) } - } else if ve.InstanceLocation == "" { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "Spacefile")) - } else { - rows = append(rows, fmt.Sprintf("%s%s", prefix, "L "+ve.InstanceLocation)) - } - - for _, c := range ve.Causes { - rows = append(rows, PrettyValidationErrors(c, v, prefix+" ")) } - return strings.Join(rows, "\n") + return fmt.Sprintf("validation failed:\n%s", strings.Join(rootErrors, "\n")) } func LoadSpacefile(projectDir string) (*Spacefile, error) { @@ -264,7 +134,10 @@ func LoadSpacefile(projectDir string) (*Spacefile, error) { if err := spacefileSchema.Validate(v); err != nil { var ve *jsonschema.ValidationError if errors.As(err, &ve) { - return nil, fmt.Errorf(PrettyValidationErrors(ve, v, "")) + return nil, SpacefileValidationError{ + wrapped: ve, + raw: v, + } } }