| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package tfjson |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| |
| "github.com/hashicorp/go-version" |
| ) |
| |
| // ValidateFormatVersionConstraints defines the versions of the JSON |
| // validate format that are supported by this package. |
| var ValidateFormatVersionConstraints = ">= 0.1, < 2.0" |
| |
| // Pos represents a position in a config file |
| type Pos struct { |
| Line int `json:"line"` |
| Column int `json:"column"` |
| Byte int `json:"byte"` |
| } |
| |
| // Range represents a range of bytes between two positions |
| type Range struct { |
| Filename string `json:"filename"` |
| Start Pos `json:"start"` |
| End Pos `json:"end"` |
| } |
| |
| type DiagnosticSeverity string |
| |
| // These severities map to the tfdiags.Severity values, plus an explicit |
| // unknown in case that enum grows without us noticing here. |
| const ( |
| DiagnosticSeverityUnknown DiagnosticSeverity = "unknown" |
| DiagnosticSeverityError DiagnosticSeverity = "error" |
| DiagnosticSeverityWarning DiagnosticSeverity = "warning" |
| ) |
| |
| // Diagnostic represents information to be presented to a user about an |
| // error or anomaly in parsing or evaluating configuration |
| type Diagnostic struct { |
| Severity DiagnosticSeverity `json:"severity,omitempty"` |
| |
| Summary string `json:"summary,omitempty"` |
| Detail string `json:"detail,omitempty"` |
| Range *Range `json:"range,omitempty"` |
| |
| Snippet *DiagnosticSnippet `json:"snippet,omitempty"` |
| } |
| |
| // DiagnosticSnippet represents source code information about the diagnostic. |
| // It is possible for a diagnostic to have a source (and therefore a range) but |
| // no source code can be found. In this case, the range field will be present and |
| // the snippet field will not. |
| type DiagnosticSnippet struct { |
| // Context is derived from HCL's hcled.ContextString output. This gives a |
| // high-level summary of the root context of the diagnostic: for example, |
| // the resource block in which an expression causes an error. |
| Context *string `json:"context"` |
| |
| // Code is a possibly-multi-line string of Terraform configuration, which |
| // includes both the diagnostic source and any relevant context as defined |
| // by the diagnostic. |
| Code string `json:"code"` |
| |
| // StartLine is the line number in the source file for the first line of |
| // the snippet code block. This is not necessarily the same as the value of |
| // Range.Start.Line, as it is possible to have zero or more lines of |
| // context source code before the diagnostic range starts. |
| StartLine int `json:"start_line"` |
| |
| // HighlightStartOffset is the character offset into Code at which the |
| // diagnostic source range starts, which ought to be highlighted as such by |
| // the consumer of this data. |
| HighlightStartOffset int `json:"highlight_start_offset"` |
| |
| // HighlightEndOffset is the character offset into Code at which the |
| // diagnostic source range ends. |
| HighlightEndOffset int `json:"highlight_end_offset"` |
| |
| // Values is a sorted slice of expression values which may be useful in |
| // understanding the source of an error in a complex expression. |
| Values []DiagnosticExpressionValue `json:"values"` |
| } |
| |
| // DiagnosticExpressionValue represents an HCL traversal string (e.g. |
| // "var.foo") and a statement about its value while the expression was |
| // evaluated (e.g. "is a string", "will be known only after apply"). These are |
| // intended to help the consumer diagnose why an expression caused a diagnostic |
| // to be emitted. |
| type DiagnosticExpressionValue struct { |
| Traversal string `json:"traversal"` |
| Statement string `json:"statement"` |
| } |
| |
| // ValidateOutput represents JSON output from terraform validate |
| // (available from 0.12 onwards) |
| type ValidateOutput struct { |
| FormatVersion string `json:"format_version"` |
| |
| Valid bool `json:"valid"` |
| ErrorCount int `json:"error_count"` |
| WarningCount int `json:"warning_count"` |
| Diagnostics []Diagnostic `json:"diagnostics"` |
| } |
| |
| // Validate checks to ensure that data is present, and the |
| // version matches the version supported by this library. |
| func (vo *ValidateOutput) Validate() error { |
| if vo == nil { |
| return errors.New("validation output is nil") |
| } |
| |
| if vo.FormatVersion == "" { |
| // The format was not versioned in the past |
| return nil |
| } |
| |
| constraint, err := version.NewConstraint(ValidateFormatVersionConstraints) |
| if err != nil { |
| return fmt.Errorf("invalid version constraint: %w", err) |
| } |
| |
| version, err := version.NewVersion(vo.FormatVersion) |
| if err != nil { |
| return fmt.Errorf("invalid format version %q: %w", vo.FormatVersion, err) |
| } |
| |
| if !constraint.Check(version) { |
| return fmt.Errorf("unsupported validation output format version: %q does not satisfy %q", |
| version, constraint) |
| } |
| |
| return nil |
| } |
| |
| func (vo *ValidateOutput) UnmarshalJSON(b []byte) error { |
| type rawOutput ValidateOutput |
| var schemas rawOutput |
| |
| err := json.Unmarshal(b, &schemas) |
| if err != nil { |
| return err |
| } |
| |
| *vo = *(*ValidateOutput)(&schemas) |
| |
| return vo.Validate() |
| } |