new 'hclpack' package
This commit is contained in:
commit
12378af8b3
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
||||
github.com/agext/levenshtein v1.2.1
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3
|
||||
github.com/apparentlymart/go-textseg v1.0.0
|
||||
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-test/deep v1.0.1
|
||||
github.com/google/go-cmp v0.2.0
|
||||
|
2
go.sum
2
go.sum
@ -6,6 +6,8 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhi
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e h1:D64GF/Xr5zSUnM3q1Jylzo4sK7szhP/ON+nb2DB5XJA=
|
||||
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
|
@ -86,8 +86,8 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
|
||||
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: fmt.Sprintf("An attribute named %q is not expected here.%s", name, suggestion),
|
||||
Summary: "Unsupported argument",
|
||||
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
@ -107,7 +107,7 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
|
||||
// Is there an attribute of the same name?
|
||||
for _, attrS := range schema.Attributes {
|
||||
if attrS.Name == blockTy {
|
||||
suggestion = fmt.Sprintf(" Did you mean to define attribute %q?", blockTy)
|
||||
suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -151,8 +151,8 @@ func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Bod
|
||||
if attrS.Required {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing required attribute",
|
||||
Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name),
|
||||
Summary: "Missing required argument",
|
||||
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
|
||||
Subject: b.MissingItemRange().Ptr(),
|
||||
})
|
||||
}
|
||||
|
24
hclpack/didyoumean.go
Normal file
24
hclpack/didyoumean.go
Normal file
@ -0,0 +1,24 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"github.com/agext/levenshtein"
|
||||
)
|
||||
|
||||
// nameSuggestion tries to find a name from the given slice of suggested names
|
||||
// that is close to the given name and returns it if found. If no suggestion
|
||||
// is close enough, returns the empty string.
|
||||
//
|
||||
// The suggestions are tried in order, so earlier suggestions take precedence
|
||||
// if the given string is similar to two or more suggestions.
|
||||
//
|
||||
// This function is intended to be used with a relatively-small number of
|
||||
// suggestions. It's not optimized for hundreds or thousands of them.
|
||||
func nameSuggestion(given string, suggestions []string) string {
|
||||
for _, suggestion := range suggestions {
|
||||
dist := levenshtein.Distance(given, suggestion, nil)
|
||||
if dist < 3 { // threshold determined experimentally
|
||||
return suggestion
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
14
hclpack/doc.go
Normal file
14
hclpack/doc.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Package hclpack provides a straightforward representation of HCL block/body
|
||||
// structure that can be easily serialized and deserialized for compact
|
||||
// transmission (e.g. over a network) without transmitting the full source code.
|
||||
//
|
||||
// Expressions are retained in native syntax source form so that their
|
||||
// evaluation can be delayed until a package structure is decoded by some
|
||||
// other system that has enough information to populate the evaluation context.
|
||||
//
|
||||
// Packed structures retain source location information but do not retain
|
||||
// actual source code. To make sense of source locations returned in diagnostics
|
||||
// and via other APIs the caller must somehow gain access to the original source
|
||||
// code that the packed representation was built from, which is a problem that
|
||||
// must be solved somehow by the calling application.
|
||||
package hclpack
|
129
hclpack/example_test.go
Normal file
129
hclpack/example_test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package hclpack_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hclpack"
|
||||
)
|
||||
|
||||
func Example_marshalJSON() {
|
||||
src := `
|
||||
service "example" {
|
||||
priority = 2
|
||||
platform {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
}
|
||||
process "web" {
|
||||
exec = ["./webapp"]
|
||||
}
|
||||
process "worker" {
|
||||
exec = ["./worker"]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
body, diags := hclpack.PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags.HasErrors() {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse: %s", diags.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jb, err := body.MarshalJSON()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to marshal: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Normally the compact form is best, but we'll indent just for the sake
|
||||
// of this example so the result is readable.
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, jb, "", " ")
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
|
||||
// Output:
|
||||
// {
|
||||
// "r": {
|
||||
// "b": [
|
||||
// {
|
||||
// "h": [
|
||||
// "service",
|
||||
// "example"
|
||||
// ],
|
||||
// "b": {
|
||||
// "a": {
|
||||
// "priority": {
|
||||
// "s": "2",
|
||||
// "r": "ChAKDA4QDhA"
|
||||
// }
|
||||
// },
|
||||
// "b": [
|
||||
// {
|
||||
// "h": [
|
||||
// "platform"
|
||||
// ],
|
||||
// "b": {
|
||||
// "a": {
|
||||
// "arch": {
|
||||
// "s": "\"amd64\"",
|
||||
// "r": "IiwiJCYsKCo"
|
||||
// },
|
||||
// "os": {
|
||||
// "s": "\"linux\"",
|
||||
// "r": "FiAWGBogHB4"
|
||||
// }
|
||||
// },
|
||||
// "r": "Li4"
|
||||
// },
|
||||
// "r": "EhQSFA"
|
||||
// },
|
||||
// {
|
||||
// "h": [
|
||||
// "process",
|
||||
// "web"
|
||||
// ],
|
||||
// "b": {
|
||||
// "a": {
|
||||
// "exec": {
|
||||
// "s": "[\"./webapp\"]",
|
||||
// "r": "OEA4OjxAPD4"
|
||||
// }
|
||||
// },
|
||||
// "r": "QkI"
|
||||
// },
|
||||
// "r": "MDYwMjQ2"
|
||||
// },
|
||||
// {
|
||||
// "h": [
|
||||
// "process",
|
||||
// "worker"
|
||||
// ],
|
||||
// "b": {
|
||||
// "a": {
|
||||
// "exec": {
|
||||
// "s": "[\"./worker\"]",
|
||||
// "r": "TFRMTlBUUFI"
|
||||
// }
|
||||
// },
|
||||
// "r": "VlY"
|
||||
// },
|
||||
// "r": "REpERkhK"
|
||||
// }
|
||||
// ],
|
||||
// "r": "WFg"
|
||||
// },
|
||||
// "r": "AggCBAYI"
|
||||
// }
|
||||
// ],
|
||||
// "r": "Wlo"
|
||||
// },
|
||||
// "s": [
|
||||
// "example.svc"
|
||||
// ],
|
||||
// "p": "BAQEAA4OAAICABISAggMABAQAAYGAAICAggIABAQAgYKAAQEAAoKAAICAAoKAAICAgYGAAgIAAYGAAICAAoKAAICAgoKAggIAA4OAAICAAoKAgwQAAgIAAYGAAICABYWAgoKAggIAA4OAAICABAQAgwQAAgIAAYGAAICABYWAgoKAgYGAgQE"
|
||||
// }
|
||||
}
|
160
hclpack/expression.go
Normal file
160
hclpack/expression.go
Normal file
@ -0,0 +1,160 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// Expression is an implementation of hcl.Expression in terms of some raw
|
||||
// expression source code. The methods of this type will first parse the
|
||||
// source code and then pass the call through to the real expression that
|
||||
// is produced.
|
||||
type Expression struct {
|
||||
// Source is the raw source code of the expression, which should be parsed
|
||||
// as the syntax specified by SourceType.
|
||||
Source []byte
|
||||
SourceType ExprSourceType
|
||||
|
||||
// Range_ and StartRange_ describe the physical extents of the expression
|
||||
// in the original source code. SourceRange_ is its entire range while
|
||||
// StartRange is just the tokens that introduce the expression type. For
|
||||
// simple expression types, SourceRange and StartRange are identical.
|
||||
Range_, StartRange_ hcl.Range
|
||||
}
|
||||
|
||||
var _ hcl.Expression = (*Expression)(nil)
|
||||
|
||||
// Value implements the Value method of hcl.Expression but with the additional
|
||||
// step of first parsing the expression source code. This implementation is
|
||||
// unusual in that it can potentially return syntax errors, whereas other
|
||||
// Value implementations usually work with already-parsed expressions.
|
||||
func (e *Expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
expr, diags := e.Parse()
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
val, moreDiags := expr.Value(ctx)
|
||||
diags = append(diags, moreDiags...)
|
||||
return val, diags
|
||||
}
|
||||
|
||||
// Variables implements the Variables method of hcl.Expression but with the
|
||||
// additional step of first parsing the expression source code.
|
||||
//
|
||||
// Since this method cannot return errors, it will return a nil slice if
|
||||
// parsing fails, indicating that no variables are present. This is okay in
|
||||
// practice because a subsequent call to Value would fail with syntax errors
|
||||
// regardless of what variables are in the context.
|
||||
func (e *Expression) Variables() []hcl.Traversal {
|
||||
expr, diags := e.Parse()
|
||||
if diags.HasErrors() {
|
||||
return nil
|
||||
}
|
||||
return expr.Variables()
|
||||
}
|
||||
|
||||
// UnwrapExpression parses and returns the underlying expression, if possible.
|
||||
//
|
||||
// This is essentially the same as Parse but without the ability to return an
|
||||
// error; it is here only to support the static analysis facilities in the
|
||||
// main HCL package (ExprList, ExprMap, etc). If any error is encountered
|
||||
// during parsing, the result is a static expression that always returns
|
||||
// cty.DynamicVal.
|
||||
//
|
||||
// This function does not impose any further conversions on the underlying
|
||||
// expression, so the result may still not be suitable for the static analysis
|
||||
// functions, depending on the source type of the expression and thus what
|
||||
// type of physical expression it becomes after decoding.
|
||||
func (e *Expression) UnwrapExpression() hcl.Expression {
|
||||
expr, diags := e.Parse()
|
||||
if diags.HasErrors() {
|
||||
return hcl.StaticExpr(cty.DynamicVal, e.Range_)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
func (e *Expression) Range() hcl.Range {
|
||||
return e.Range_
|
||||
}
|
||||
|
||||
func (e *Expression) StartRange() hcl.Range {
|
||||
return e.StartRange_
|
||||
}
|
||||
|
||||
// Parse attempts to parse the source code of the receiving expression using
|
||||
// its indicated source type, returning the expression if possible and any
|
||||
// diagnostics produced during parsing.
|
||||
func (e *Expression) Parse() (hcl.Expression, hcl.Diagnostics) {
|
||||
switch e.SourceType {
|
||||
case ExprNative:
|
||||
return hclsyntax.ParseExpression(e.Source, e.Range_.Filename, e.Range_.Start)
|
||||
case ExprTemplate:
|
||||
return hclsyntax.ParseTemplate(e.Source, e.Range_.Filename, e.Range_.Start)
|
||||
case ExprLiteralJSON:
|
||||
ty, err := ctyjson.ImpliedType(e.Source)
|
||||
if err != nil {
|
||||
return nil, hcl.Diagnostics{
|
||||
{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid JSON value",
|
||||
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
|
||||
Subject: &e.Range_,
|
||||
},
|
||||
}
|
||||
}
|
||||
val, err := ctyjson.Unmarshal(e.Source, ty)
|
||||
if err != nil {
|
||||
return nil, hcl.Diagnostics{
|
||||
{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid JSON value",
|
||||
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
|
||||
Subject: &e.Range_,
|
||||
},
|
||||
}
|
||||
}
|
||||
return hcl.StaticExpr(val, e.Range_), nil
|
||||
default:
|
||||
// This should never happen for a valid Expression.
|
||||
return nil, hcl.Diagnostics{
|
||||
{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid expression source type",
|
||||
Detail: fmt.Sprintf("Packed version of this expression has an invalid source type %s. This is always a bug.", e.SourceType),
|
||||
Subject: &e.Range_,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expression) addRanges(rngs map[hcl.Range]struct{}) {
|
||||
rngs[e.Range_] = struct{}{}
|
||||
rngs[e.StartRange_] = struct{}{}
|
||||
}
|
||||
|
||||
// ExprSourceType defines the syntax type used for an expression's source code,
|
||||
// which is then used to select a suitable parser for it when evaluating.
|
||||
type ExprSourceType rune
|
||||
|
||||
//go:generate stringer -type ExprSourceType
|
||||
|
||||
const (
|
||||
// ExprNative indicates that an expression must be parsed as native
|
||||
// expression syntax, with hclsyntax.ParseExpression.
|
||||
ExprNative ExprSourceType = 'N'
|
||||
|
||||
// ExprTemplate indicates that an expression must be parsed as native
|
||||
// template syntax, with hclsyntax.ParseTemplate.
|
||||
ExprTemplate ExprSourceType = 'T'
|
||||
|
||||
// ExprLiteralJSON indicates that an expression must be parsed as JSON and
|
||||
// treated literally, using cty/json. This can be used when populating
|
||||
// literal attribute values from a non-HCL source.
|
||||
ExprLiteralJSON ExprSourceType = 'L'
|
||||
)
|
70
hclpack/expression_test.go
Normal file
70
hclpack/expression_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestExpressionValue(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Expr *Expression
|
||||
Ctx *hcl.EvalContext
|
||||
Want cty.Value
|
||||
}{
|
||||
"simple literal expr": {
|
||||
&Expression{
|
||||
Source: []byte(`"hello"`),
|
||||
SourceType: ExprNative,
|
||||
},
|
||||
nil,
|
||||
cty.StringVal("hello"),
|
||||
},
|
||||
"simple literal template": {
|
||||
&Expression{
|
||||
Source: []byte(`hello ${5}`),
|
||||
SourceType: ExprTemplate,
|
||||
},
|
||||
nil,
|
||||
cty.StringVal("hello 5"),
|
||||
},
|
||||
"expr with variable": {
|
||||
&Expression{
|
||||
Source: []byte(`foo`),
|
||||
SourceType: ExprNative,
|
||||
},
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
},
|
||||
},
|
||||
cty.StringVal("bar"),
|
||||
},
|
||||
"template with variable": {
|
||||
&Expression{
|
||||
Source: []byte(`foo ${foo}`),
|
||||
SourceType: ExprTemplate,
|
||||
},
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
},
|
||||
},
|
||||
cty.StringVal("foo bar"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := test.Expr.Value(test.Ctx)
|
||||
for _, diag := range diags {
|
||||
t.Errorf("unexpected diagnostic: %s", diag.Error())
|
||||
}
|
||||
|
||||
if !test.Want.RawEquals(got) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
21
hclpack/exprsourcetype_string.go
Normal file
21
hclpack/exprsourcetype_string.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Code generated by "stringer -type ExprSourceType"; DO NOT EDIT.
|
||||
|
||||
package hclpack
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
_ExprSourceType_name_0 = "ExprNative"
|
||||
_ExprSourceType_name_1 = "ExprTemplate"
|
||||
)
|
||||
|
||||
func (i ExprSourceType) String() string {
|
||||
switch {
|
||||
case i == 78:
|
||||
return _ExprSourceType_name_0
|
||||
case i == 84:
|
||||
return _ExprSourceType_name_1
|
||||
default:
|
||||
return "ExprSourceType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
211
hclpack/json_marshal.go
Normal file
211
hclpack/json_marshal.go
Normal file
@ -0,0 +1,211 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
// MarshalJSON is an implementation of Marshaler from encoding/json, allowing
|
||||
// bodies to be included in other types that are JSON-marshalable.
|
||||
//
|
||||
// The result of MarshalJSON is optimized for compactness rather than easy
|
||||
// human consumption/editing. Use UnmarshalJSON to decode it.
|
||||
func (b *Body) MarshalJSON() ([]byte, error) {
|
||||
rngs := make(map[hcl.Range]struct{})
|
||||
b.addRanges(rngs)
|
||||
|
||||
fns, posList, posMap := packPositions(rngs)
|
||||
|
||||
head := jsonHeader{
|
||||
Body: b.forJSON(posMap),
|
||||
Sources: fns,
|
||||
Pos: posList,
|
||||
}
|
||||
|
||||
return json.Marshal(&head)
|
||||
}
|
||||
|
||||
func (b *Body) forJSON(pos map[string]map[hcl.Pos]posOfs) bodyJSON {
|
||||
var ret bodyJSON
|
||||
|
||||
if len(b.Attributes) > 0 {
|
||||
ret.Attrs = make(map[string]attrJSON, len(b.Attributes))
|
||||
for name, attr := range b.Attributes {
|
||||
ret.Attrs[name] = attr.forJSON(pos)
|
||||
}
|
||||
}
|
||||
if len(b.ChildBlocks) > 0 {
|
||||
ret.Blocks = make([]blockJSON, len(b.ChildBlocks))
|
||||
for i, block := range b.ChildBlocks {
|
||||
ret.Blocks[i] = block.forJSON(pos)
|
||||
}
|
||||
}
|
||||
ret.Ranges = make(rangesPacked, 1)
|
||||
ret.Ranges[0] = packRange(b.MissingItemRange_, pos)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (a *Attribute) forJSON(pos map[string]map[hcl.Pos]posOfs) attrJSON {
|
||||
var ret attrJSON
|
||||
|
||||
ret.Source = string(a.Expr.Source)
|
||||
switch a.Expr.SourceType {
|
||||
case ExprNative:
|
||||
ret.Syntax = 0
|
||||
case ExprTemplate:
|
||||
ret.Syntax = 1
|
||||
case ExprLiteralJSON:
|
||||
ret.Syntax = 2
|
||||
}
|
||||
ret.Ranges = make(rangesPacked, 4)
|
||||
ret.Ranges[0] = packRange(a.Range, pos)
|
||||
ret.Ranges[1] = packRange(a.NameRange, pos)
|
||||
ret.Ranges[2] = packRange(a.Expr.Range_, pos)
|
||||
ret.Ranges[3] = packRange(a.Expr.StartRange_, pos)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *Block) forJSON(pos map[string]map[hcl.Pos]posOfs) blockJSON {
|
||||
var ret blockJSON
|
||||
|
||||
ret.Header = make([]string, len(b.Labels)+1)
|
||||
ret.Header[0] = b.Type
|
||||
copy(ret.Header[1:], b.Labels)
|
||||
ret.Body = b.Body.forJSON(pos)
|
||||
ret.Ranges = make(rangesPacked, 2+len(b.LabelRanges))
|
||||
ret.Ranges[0] = packRange(b.DefRange, pos)
|
||||
ret.Ranges[1] = packRange(b.TypeRange, pos)
|
||||
for i, rng := range b.LabelRanges {
|
||||
ret.Ranges[i+2] = packRange(rng, pos)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// UnmarshalJSON is an implementation of Unmarshaler from encoding/json,
|
||||
// allowing bodies to be included in other types that are JSON-unmarshalable.
|
||||
func (b *Body) UnmarshalJSON(data []byte) error {
|
||||
var head jsonHeader
|
||||
err := json.Unmarshal(data, &head)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fns := head.Sources
|
||||
positions := head.Pos.Unpack()
|
||||
|
||||
*b = head.Body.decode(fns, positions)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonHeader struct {
|
||||
Body bodyJSON `json:"r"`
|
||||
|
||||
Sources []string `json:"s,omitempty"`
|
||||
Pos positionsPacked `json:"p,omitempty"`
|
||||
}
|
||||
|
||||
type bodyJSON struct {
|
||||
// Files are the source filenames that were involved in
|
||||
Attrs map[string]attrJSON `json:"a,omitempty"`
|
||||
Blocks []blockJSON `json:"b,omitempty"`
|
||||
|
||||
// Ranges contains the MissingItemRange
|
||||
Ranges rangesPacked `json:"r,omitempty"`
|
||||
}
|
||||
|
||||
func (bj *bodyJSON) decode(fns []string, positions []position) Body {
|
||||
var ret Body
|
||||
|
||||
if len(bj.Attrs) > 0 {
|
||||
ret.Attributes = make(map[string]Attribute, len(bj.Attrs))
|
||||
for name, aj := range bj.Attrs {
|
||||
ret.Attributes[name] = aj.decode(fns, positions)
|
||||
}
|
||||
}
|
||||
|
||||
if len(bj.Blocks) > 0 {
|
||||
ret.ChildBlocks = make([]Block, len(bj.Blocks))
|
||||
for i, blj := range bj.Blocks {
|
||||
ret.ChildBlocks[i] = blj.decode(fns, positions)
|
||||
}
|
||||
}
|
||||
|
||||
ret.MissingItemRange_ = bj.Ranges.UnpackIdx(fns, positions, 0)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type attrJSON struct {
|
||||
// To keep things compact, in the JSON encoding we flatten the
|
||||
// expression down into the attribute object, since overhead
|
||||
// for attributes adds up in a complex config.
|
||||
Source string `json:"s"`
|
||||
Syntax int `json:"t,omitempty"` // omitted for 0=native
|
||||
|
||||
// Ranges contains the Range, NameRange, Expr.Range, Expr.StartRange
|
||||
Ranges rangesPacked `json:"r,omitempty"`
|
||||
}
|
||||
|
||||
func (aj *attrJSON) decode(fns []string, positions []position) Attribute {
|
||||
var ret Attribute
|
||||
|
||||
ret.Expr.Source = []byte(aj.Source)
|
||||
switch aj.Syntax {
|
||||
case 0:
|
||||
ret.Expr.SourceType = ExprNative
|
||||
case 1:
|
||||
ret.Expr.SourceType = ExprTemplate
|
||||
case 2:
|
||||
ret.Expr.SourceType = ExprLiteralJSON
|
||||
}
|
||||
|
||||
ret.Range = aj.Ranges.UnpackIdx(fns, positions, 0)
|
||||
ret.NameRange = aj.Ranges.UnpackIdx(fns, positions, 1)
|
||||
ret.Expr.Range_ = aj.Ranges.UnpackIdx(fns, positions, 2)
|
||||
ret.Expr.StartRange_ = aj.Ranges.UnpackIdx(fns, positions, 3)
|
||||
if ret.Expr.StartRange_ == (hcl.Range{}) {
|
||||
// If the start range wasn't present then we'll just use the Range
|
||||
ret.Expr.StartRange_ = ret.Expr.Range_
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type blockJSON struct {
|
||||
// Header is the type followed by any labels. We flatten this here
|
||||
// to keep the JSON encoding compact.
|
||||
Header []string `json:"h"`
|
||||
Body bodyJSON `json:"b,omitempty"`
|
||||
|
||||
// Ranges contains the DefRange followed by the TypeRange and then
|
||||
// each of the label ranges in turn.
|
||||
Ranges rangesPacked `json:"r,omitempty"`
|
||||
}
|
||||
|
||||
func (blj *blockJSON) decode(fns []string, positions []position) Block {
|
||||
var ret Block
|
||||
|
||||
if len(blj.Header) > 0 { // If the header is invalid then we'll end up with an empty type
|
||||
ret.Type = blj.Header[0]
|
||||
}
|
||||
if len(blj.Header) > 1 {
|
||||
ret.Labels = blj.Header[1:]
|
||||
}
|
||||
ret.Body = blj.Body.decode(fns, positions)
|
||||
|
||||
ret.DefRange = blj.Ranges.UnpackIdx(fns, positions, 0)
|
||||
ret.TypeRange = blj.Ranges.UnpackIdx(fns, positions, 1)
|
||||
if len(ret.Labels) > 0 {
|
||||
ret.LabelRanges = make([]hcl.Range, len(ret.Labels))
|
||||
for i := range ret.Labels {
|
||||
ret.LabelRanges[i] = blj.Ranges.UnpackIdx(fns, positions, i+2)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
47
hclpack/json_marshal_test.go
Normal file
47
hclpack/json_marshal_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func TestJSONRoundTrip(t *testing.T) {
|
||||
src := `
|
||||
service "example" {
|
||||
priority = 2
|
||||
platform {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
}
|
||||
process "web" {
|
||||
exec = ["./webapp"]
|
||||
}
|
||||
process "worker" {
|
||||
exec = ["./worker"]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
startBody, diags := PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("Failed to parse: %s", diags.Error())
|
||||
}
|
||||
|
||||
jb, err := startBody.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal: %s", err)
|
||||
}
|
||||
|
||||
endBody := &Body{}
|
||||
err = endBody.UnmarshalJSON(jb)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal: %s", err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(startBody, endBody) {
|
||||
t.Errorf("incorrect result\n%s", cmp.Diff(startBody, endBody))
|
||||
}
|
||||
}
|
58
hclpack/pack_native.go
Normal file
58
hclpack/pack_native.go
Normal file
@ -0,0 +1,58 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// PackNativeFile parses the given source code as HCL native syntax and packs
|
||||
// it into a hclpack Body ready to be marshalled.
|
||||
//
|
||||
// If the given source code contains syntax errors then error diagnostics will
|
||||
// be returned. A non-nil body might still be returned in this case, which
|
||||
// allows a cautious caller to still do certain analyses on the result.
|
||||
func PackNativeFile(src []byte, filename string, start hcl.Pos) (*Body, hcl.Diagnostics) {
|
||||
f, diags := hclsyntax.ParseConfig(src, filename, start)
|
||||
rootBody := f.Body.(*hclsyntax.Body)
|
||||
return packNativeBody(rootBody, src), diags
|
||||
}
|
||||
|
||||
func packNativeBody(body *hclsyntax.Body, src []byte) *Body {
|
||||
ret := &Body{}
|
||||
for name, attr := range body.Attributes {
|
||||
exprRng := attr.Expr.Range()
|
||||
exprStartRng := attr.Expr.StartRange()
|
||||
exprSrc := exprRng.SliceBytes(src)
|
||||
ret.setAttribute(name, Attribute{
|
||||
Expr: Expression{
|
||||
Source: exprSrc,
|
||||
SourceType: ExprNative,
|
||||
|
||||
Range_: exprRng,
|
||||
StartRange_: exprStartRng,
|
||||
},
|
||||
Range: attr.Range(),
|
||||
NameRange: attr.NameRange,
|
||||
})
|
||||
}
|
||||
|
||||
for _, block := range body.Blocks {
|
||||
childBody := packNativeBody(block.Body, src)
|
||||
defRange := block.TypeRange
|
||||
if len(block.LabelRanges) > 0 {
|
||||
defRange = hcl.RangeBetween(defRange, block.LabelRanges[len(block.LabelRanges)-1])
|
||||
}
|
||||
ret.appendBlock(Block{
|
||||
Type: block.Type,
|
||||
Labels: block.Labels,
|
||||
Body: *childBody,
|
||||
TypeRange: block.TypeRange,
|
||||
DefRange: defRange,
|
||||
LabelRanges: block.LabelRanges,
|
||||
})
|
||||
}
|
||||
|
||||
ret.MissingItemRange_ = body.EndRange
|
||||
|
||||
return ret
|
||||
}
|
166
hclpack/pack_native_test.go
Normal file
166
hclpack/pack_native_test.go
Normal file
@ -0,0 +1,166 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func TestPackNativeFile(t *testing.T) {
|
||||
src := `
|
||||
foo = "bar"
|
||||
baz = "boz"
|
||||
|
||||
child {
|
||||
a = b + c
|
||||
}
|
||||
|
||||
another_child "foo" "bar" {}
|
||||
`
|
||||
|
||||
got, diags := PackNativeFile([]byte(src), "", hcl.Pos{Line: 1, Column: 1})
|
||||
for _, diag := range diags {
|
||||
t.Errorf("unexpected diagnostic: %s", diag.Error())
|
||||
}
|
||||
|
||||
want := &Body{
|
||||
Attributes: map[string]Attribute{
|
||||
"baz": {
|
||||
Expr: Expression{
|
||||
Source: []byte(`"boz"`),
|
||||
SourceType: ExprNative,
|
||||
Range_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 3, Column: 7, Byte: 19},
|
||||
End: hcl.Pos{Line: 3, Column: 12, Byte: 24},
|
||||
},
|
||||
StartRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 3, Column: 8, Byte: 20},
|
||||
End: hcl.Pos{Line: 3, Column: 11, Byte: 23},
|
||||
},
|
||||
},
|
||||
Range: hcl.Range{
|
||||
Start: hcl.Pos{Line: 3, Column: 1, Byte: 13},
|
||||
End: hcl.Pos{Line: 3, Column: 12, Byte: 24},
|
||||
},
|
||||
NameRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 3, Column: 1, Byte: 13},
|
||||
End: hcl.Pos{Line: 3, Column: 4, Byte: 16},
|
||||
},
|
||||
},
|
||||
"foo": {
|
||||
Expr: Expression{
|
||||
Source: []byte(`"bar"`),
|
||||
SourceType: ExprNative,
|
||||
Range_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 2, Column: 7, Byte: 7},
|
||||
End: hcl.Pos{Line: 2, Column: 12, Byte: 12},
|
||||
},
|
||||
StartRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 2, Column: 8, Byte: 8},
|
||||
End: hcl.Pos{Line: 2, Column: 11, Byte: 11},
|
||||
},
|
||||
},
|
||||
Range: hcl.Range{
|
||||
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
|
||||
End: hcl.Pos{Line: 2, Column: 12, Byte: 12},
|
||||
},
|
||||
NameRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
|
||||
End: hcl.Pos{Line: 2, Column: 4, Byte: 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
ChildBlocks: []Block{
|
||||
{
|
||||
Type: "child",
|
||||
Body: Body{
|
||||
Attributes: map[string]Attribute{
|
||||
"a": {
|
||||
Expr: Expression{
|
||||
Source: []byte(`b + c`),
|
||||
SourceType: ExprNative,
|
||||
Range_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 6, Column: 7, Byte: 40},
|
||||
End: hcl.Pos{Line: 6, Column: 12, Byte: 45},
|
||||
},
|
||||
StartRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 6, Column: 7, Byte: 40},
|
||||
End: hcl.Pos{Line: 6, Column: 8, Byte: 41},
|
||||
},
|
||||
},
|
||||
Range: hcl.Range{
|
||||
Start: hcl.Pos{Line: 6, Column: 3, Byte: 36},
|
||||
End: hcl.Pos{Line: 6, Column: 12, Byte: 45},
|
||||
},
|
||||
NameRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 6, Column: 3, Byte: 36},
|
||||
End: hcl.Pos{Line: 6, Column: 4, Byte: 37},
|
||||
},
|
||||
},
|
||||
},
|
||||
MissingItemRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 7, Column: 2, Byte: 47},
|
||||
End: hcl.Pos{Line: 7, Column: 2, Byte: 47},
|
||||
},
|
||||
},
|
||||
DefRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 5, Column: 1, Byte: 26},
|
||||
End: hcl.Pos{Line: 5, Column: 6, Byte: 31},
|
||||
},
|
||||
TypeRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 5, Column: 1, Byte: 26},
|
||||
End: hcl.Pos{Line: 5, Column: 6, Byte: 31},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "another_child",
|
||||
Labels: []string{"foo", "bar"},
|
||||
Body: Body{
|
||||
MissingItemRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 9, Column: 29, Byte: 77},
|
||||
End: hcl.Pos{Line: 9, Column: 29, Byte: 77},
|
||||
},
|
||||
},
|
||||
DefRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 9, Column: 1, Byte: 49},
|
||||
End: hcl.Pos{Line: 9, Column: 26, Byte: 74},
|
||||
},
|
||||
TypeRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 9, Column: 1, Byte: 49},
|
||||
End: hcl.Pos{Line: 9, Column: 14, Byte: 62},
|
||||
},
|
||||
LabelRanges: []hcl.Range{
|
||||
hcl.Range{
|
||||
Start: hcl.Pos{Line: 9, Column: 15, Byte: 63},
|
||||
End: hcl.Pos{Line: 9, Column: 20, Byte: 68},
|
||||
},
|
||||
hcl.Range{
|
||||
Start: hcl.Pos{Line: 9, Column: 21, Byte: 69},
|
||||
End: hcl.Pos{Line: 9, Column: 26, Byte: 74},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MissingItemRange_: hcl.Range{
|
||||
Start: hcl.Pos{Line: 10, Column: 1, Byte: 78},
|
||||
End: hcl.Pos{Line: 10, Column: 1, Byte: 78},
|
||||
},
|
||||
}
|
||||
|
||||
if !cmp.Equal(want, got) {
|
||||
bytesAsString := func(s []byte) string {
|
||||
return string(s)
|
||||
}
|
||||
posAsString := func(pos hcl.Pos) string {
|
||||
return fmt.Sprintf("%#v", pos)
|
||||
}
|
||||
t.Errorf("wrong result\n%s", cmp.Diff(
|
||||
want, got,
|
||||
cmp.Transformer("bytesAsString", bytesAsString),
|
||||
cmp.Transformer("posAsString", posAsString),
|
||||
))
|
||||
}
|
||||
}
|
324
hclpack/positions_packed.go
Normal file
324
hclpack/positions_packed.go
Normal file
@ -0,0 +1,324 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
// positionsPacked is a delta-based representation of source positions
|
||||
// that implements encoding.TextMarshaler and encoding.TextUnmarshaler using
|
||||
// a compact variable-length quantity encoding to mimimize the overhead of
|
||||
// storing source positions.
|
||||
//
|
||||
// Serializations of the other types in this package can refer to positions
|
||||
// in a positionsPacked by index.
|
||||
type positionsPacked []positionPacked
|
||||
|
||||
func (pp positionsPacked) MarshalBinary() ([]byte, error) {
|
||||
lenInt := len(pp) * 4 // each positionPacked contains four ints, but we don't include the fileidx
|
||||
|
||||
// guess avg of ~1.25 bytes per int, in which case we'll avoid further allocation
|
||||
buf := newVLQBuf(lenInt + (lenInt / 4))
|
||||
var lastFileIdx int
|
||||
for _, ppr := range pp {
|
||||
// Rather than writing out the same file index over and over, we instead
|
||||
// insert a ; delimiter each time it increases. Since it's common for
|
||||
// for a body to be entirely in one file, this can lead to considerable
|
||||
// savings in that case.
|
||||
delims := ppr.FileIdx - lastFileIdx
|
||||
for i := 0; i < delims; i++ {
|
||||
buf = buf.AppendRawByte(';')
|
||||
}
|
||||
buf = buf.AppendInt(ppr.LineDelta)
|
||||
buf = buf.AppendInt(ppr.ColumnDelta)
|
||||
buf = buf.AppendInt(ppr.ByteDelta)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (pp positionsPacked) MarshalText() ([]byte, error) {
|
||||
raw, err := pp.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := base64.RawStdEncoding.EncodedLen(len(raw))
|
||||
ret := make([]byte, l)
|
||||
base64.RawStdEncoding.Encode(ret, raw)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (pp *positionsPacked) UnmarshalBinary(data []byte) error {
|
||||
buf := vlqBuf(data)
|
||||
var ret positionsPacked
|
||||
fileIdx := 0
|
||||
for len(buf) > 0 {
|
||||
if buf[0] == ';' {
|
||||
// Starting a new file, then.
|
||||
fileIdx++
|
||||
buf = buf[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
var ppr positionPacked
|
||||
var err error
|
||||
ppr.LineDelta, buf, err = buf.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ppr.ColumnDelta, buf, err = buf.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ppr.ByteDelta, buf, err = buf.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret = append(ret, ppr)
|
||||
}
|
||||
*pp = ret
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *positionsPacked) UnmarshalText(data []byte) error {
|
||||
maxL := base64.RawStdEncoding.DecodedLen(len(data))
|
||||
into := make([]byte, maxL)
|
||||
realL, err := base64.RawStdEncoding.Decode(into, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pp.UnmarshalBinary(into[:realL])
|
||||
}
|
||||
|
||||
type position struct {
|
||||
FileIdx int
|
||||
Pos hcl.Pos
|
||||
}
|
||||
|
||||
func (pp positionsPacked) Unpack() []position {
|
||||
ret := make([]position, len(pp))
|
||||
var accPos hcl.Pos
|
||||
var accFileIdx int
|
||||
|
||||
for i, relPos := range pp {
|
||||
if relPos.FileIdx != accFileIdx {
|
||||
accPos = hcl.Pos{} // reset base position for each new file
|
||||
accFileIdx = pp[i].FileIdx
|
||||
}
|
||||
if relPos.LineDelta > 0 {
|
||||
accPos.Column = 0 // reset column position for each new line
|
||||
}
|
||||
accPos.Line += relPos.LineDelta
|
||||
accPos.Column += relPos.ColumnDelta
|
||||
accPos.Byte += relPos.ByteDelta
|
||||
ret[i] = position{
|
||||
FileIdx: relPos.FileIdx,
|
||||
Pos: accPos,
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type positionPacked struct {
|
||||
FileIdx int
|
||||
LineDelta, ColumnDelta, ByteDelta int
|
||||
}
|
||||
|
||||
func (pp positionsPacked) Len() int {
|
||||
return len(pp)
|
||||
}
|
||||
|
||||
func (pp positionsPacked) Less(i, j int) bool {
|
||||
return pp[i].FileIdx < pp[j].FileIdx
|
||||
}
|
||||
|
||||
func (pp positionsPacked) Swap(i, j int) {
|
||||
pp[i], pp[j] = pp[j], pp[i]
|
||||
}
|
||||
|
||||
// posOfs is an index into a positionsPacked. The zero value of this type
|
||||
// represents the absense of a position.
|
||||
type posOfs int
|
||||
|
||||
func newPosOffs(idx int) posOfs {
|
||||
return posOfs(idx + 1)
|
||||
}
|
||||
|
||||
func (o posOfs) Index() int {
|
||||
return int(o - 1)
|
||||
}
|
||||
|
||||
// rangePacked is a range represented as two indexes into a positionsPacked.
|
||||
// This implements encoding.TextMarshaler and encoding.TextUnmarshaler using
|
||||
// a compact variable-length quantity encoding.
|
||||
type rangePacked struct {
|
||||
Start posOfs
|
||||
End posOfs
|
||||
}
|
||||
|
||||
func packRange(rng hcl.Range, pos map[string]map[hcl.Pos]posOfs) rangePacked {
|
||||
return rangePacked{
|
||||
Start: pos[rng.Filename][rng.Start],
|
||||
End: pos[rng.Filename][rng.End],
|
||||
}
|
||||
}
|
||||
|
||||
func (rp rangePacked) Unpack(fns []string, poss []position) hcl.Range {
|
||||
startIdx := rp.Start.Index()
|
||||
endIdx := rp.End.Index()
|
||||
if startIdx < 0 && startIdx >= len(poss) {
|
||||
return hcl.Range{} // out of bounds, so invalid
|
||||
}
|
||||
if endIdx < 0 && endIdx >= len(poss) {
|
||||
return hcl.Range{} // out of bounds, so invalid
|
||||
}
|
||||
startPos := poss[startIdx]
|
||||
endPos := poss[endIdx]
|
||||
fnIdx := startPos.FileIdx
|
||||
var fn string
|
||||
if fnIdx >= 0 && fnIdx < len(fns) {
|
||||
fn = fns[fnIdx]
|
||||
}
|
||||
return hcl.Range{
|
||||
Filename: fn,
|
||||
Start: startPos.Pos,
|
||||
End: endPos.Pos,
|
||||
}
|
||||
}
|
||||
|
||||
// rangesPacked represents a sequence of ranges, packed compactly into a single
|
||||
// string during marshaling.
|
||||
type rangesPacked []rangePacked
|
||||
|
||||
func (rp rangesPacked) MarshalBinary() ([]byte, error) {
|
||||
lenInt := len(rp) * 2 // each positionPacked contains two ints
|
||||
|
||||
// guess avg of ~1.25 bytes per int, in which case we'll avoid further allocation
|
||||
buf := newVLQBuf(lenInt + (lenInt / 4))
|
||||
for _, rpr := range rp {
|
||||
buf = buf.AppendInt(int(rpr.Start)) // intentionally storing these as 1-based offsets
|
||||
buf = buf.AppendInt(int(rpr.End))
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (rp rangesPacked) MarshalText() ([]byte, error) {
|
||||
raw, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := base64.RawStdEncoding.EncodedLen(len(raw))
|
||||
ret := make([]byte, l)
|
||||
base64.RawStdEncoding.Encode(ret, raw)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (rp *rangesPacked) UnmarshalBinary(data []byte) error {
|
||||
buf := vlqBuf(data)
|
||||
var ret rangesPacked
|
||||
for len(buf) > 0 {
|
||||
var startInt, endInt int
|
||||
var err error
|
||||
startInt, buf, err = buf.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endInt, buf, err = buf.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret = append(ret, rangePacked{
|
||||
Start: posOfs(startInt), // these are stored as 1-based offsets, so safe to convert directly
|
||||
End: posOfs(endInt),
|
||||
})
|
||||
}
|
||||
*rp = ret
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rp *rangesPacked) UnmarshalText(data []byte) error {
|
||||
maxL := base64.RawStdEncoding.DecodedLen(len(data))
|
||||
into := make([]byte, maxL)
|
||||
realL, err := base64.RawStdEncoding.Decode(into, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rp.UnmarshalBinary(into[:realL])
|
||||
}
|
||||
|
||||
func (rps rangesPacked) UnpackIdx(fns []string, poss []position, idx int) hcl.Range {
|
||||
if idx < 0 || idx >= len(rps) {
|
||||
return hcl.Range{} // out of bounds, so invalid
|
||||
}
|
||||
return rps[idx].Unpack(fns, poss)
|
||||
}
|
||||
|
||||
// packPositions will find the distinct positions from the given ranges
|
||||
// and then pack them into a positionsPacked, along with a lookup table to find
|
||||
// the encoded offset of each distinct position.
|
||||
func packPositions(rngs map[hcl.Range]struct{}) (fns []string, poss positionsPacked, posMap map[string]map[hcl.Pos]posOfs) {
|
||||
const noOfs = posOfs(0)
|
||||
|
||||
posByFile := make(map[string][]hcl.Pos)
|
||||
for rng := range rngs {
|
||||
fn := rng.Filename
|
||||
posByFile[fn] = append(posByFile[fn], rng.Start)
|
||||
posByFile[fn] = append(posByFile[fn], rng.End)
|
||||
}
|
||||
fns = make([]string, 0, len(posByFile))
|
||||
for fn := range posByFile {
|
||||
fns = append(fns, fn)
|
||||
}
|
||||
sort.Strings(fns)
|
||||
|
||||
var retPos positionsPacked
|
||||
posMap = make(map[string]map[hcl.Pos]posOfs)
|
||||
for fileIdx, fn := range fns {
|
||||
poss := posByFile[fn]
|
||||
sort.Sort(sortPositions(poss))
|
||||
var prev hcl.Pos
|
||||
for _, pos := range poss {
|
||||
if _, exists := posMap[fn][pos]; exists {
|
||||
continue
|
||||
}
|
||||
ofs := newPosOffs(len(retPos))
|
||||
if pos.Line != prev.Line {
|
||||
// Column indices start from zero for each new line.
|
||||
prev.Column = 0
|
||||
}
|
||||
retPos = append(retPos, positionPacked{
|
||||
FileIdx: fileIdx,
|
||||
LineDelta: pos.Line - prev.Line,
|
||||
ColumnDelta: pos.Column - prev.Column,
|
||||
ByteDelta: pos.Byte - prev.Byte,
|
||||
})
|
||||
if posMap[fn] == nil {
|
||||
posMap[fn] = make(map[hcl.Pos]posOfs)
|
||||
}
|
||||
posMap[fn][pos] = ofs
|
||||
prev = pos
|
||||
}
|
||||
}
|
||||
|
||||
return fns, retPos, posMap
|
||||
}
|
||||
|
||||
type sortPositions []hcl.Pos
|
||||
|
||||
func (sp sortPositions) Len() int {
|
||||
return len(sp)
|
||||
}
|
||||
|
||||
func (sp sortPositions) Less(i, j int) bool {
|
||||
return sp[i].Byte < sp[j].Byte
|
||||
}
|
||||
|
||||
func (sp sortPositions) Swap(i, j int) {
|
||||
sp[i], sp[j] = sp[j], sp[i]
|
||||
}
|
290
hclpack/structure.go
Normal file
290
hclpack/structure.go
Normal file
@ -0,0 +1,290 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
// Body is an implementation of hcl.Body.
|
||||
type Body struct {
|
||||
Attributes map[string]Attribute
|
||||
ChildBlocks []Block
|
||||
|
||||
MissingItemRange_ hcl.Range
|
||||
}
|
||||
|
||||
var _ hcl.Body = (*Body)(nil)
|
||||
|
||||
// Content is an implementation of the method of the same name on hcl.Body.
|
||||
//
|
||||
// When Content is called directly on a hclpack.Body, all child block bodies
|
||||
// are guaranteed to be of type *hclpack.Body, so callers can type-assert
|
||||
// to obtain a child Body in order to serialize it separately if needed.
|
||||
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
|
||||
return b.content(schema, nil)
|
||||
}
|
||||
|
||||
// PartialContent is an implementation of the method of the same name on hcl.Body.
|
||||
//
|
||||
// The returned "remain" body may share some backing objects with the receiver,
|
||||
// so neither the receiver nor the returned remain body, or any descendent
|
||||
// objects within them, may be mutated after this method is used.
|
||||
//
|
||||
// When Content is called directly on a hclpack.Body, all child block bodies
|
||||
// and the returned "remain" body are guaranteed to be of type *hclpack.Body,
|
||||
// so callers can type-assert to obtain a child Body in order to serialize it
|
||||
// separately if needed.
|
||||
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
|
||||
remain := &Body{}
|
||||
content, diags := b.content(schema, remain)
|
||||
return content, remain, diags
|
||||
}
|
||||
|
||||
func (b *Body) content(schema *hcl.BodySchema, remain *Body) (*hcl.BodyContent, hcl.Diagnostics) {
|
||||
if b == nil {
|
||||
b = &Body{} // We'll treat a nil body like an empty one, for convenience
|
||||
}
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
var attrs map[string]*hcl.Attribute
|
||||
var attrUsed map[string]struct{}
|
||||
if len(b.Attributes) > 0 {
|
||||
attrs = make(map[string]*hcl.Attribute, len(b.Attributes))
|
||||
attrUsed = make(map[string]struct{}, len(b.Attributes))
|
||||
}
|
||||
for _, attrS := range schema.Attributes {
|
||||
name := attrS.Name
|
||||
attr, exists := b.Attributes[name]
|
||||
if !exists {
|
||||
if attrS.Required {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing required argument",
|
||||
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
|
||||
Subject: &b.MissingItemRange_,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
attrs[name] = attr.asHCLAttribute(name)
|
||||
attrUsed[name] = struct{}{}
|
||||
}
|
||||
|
||||
for name, attr := range b.Attributes {
|
||||
if _, used := attrUsed[name]; used {
|
||||
continue
|
||||
}
|
||||
if remain != nil {
|
||||
remain.setAttribute(name, attr)
|
||||
continue
|
||||
}
|
||||
var suggestions []string
|
||||
for _, attrS := range schema.Attributes {
|
||||
if _, defined := attrs[name]; defined {
|
||||
continue
|
||||
}
|
||||
suggestions = append(suggestions, attrS.Name)
|
||||
}
|
||||
suggestion := nameSuggestion(name, suggestions)
|
||||
if suggestion != "" {
|
||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
||||
} else {
|
||||
// Is there a block of the same name?
|
||||
for _, blockS := range schema.Blocks {
|
||||
if blockS.Type == name {
|
||||
suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unsupported argument",
|
||||
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
|
||||
blocksWanted := make(map[string]hcl.BlockHeaderSchema)
|
||||
for _, blockS := range schema.Blocks {
|
||||
blocksWanted[blockS.Type] = blockS
|
||||
}
|
||||
|
||||
var blocks []*hcl.Block
|
||||
for _, block := range b.ChildBlocks {
|
||||
blockTy := block.Type
|
||||
blockS, wanted := blocksWanted[blockTy]
|
||||
if !wanted {
|
||||
if remain != nil {
|
||||
remain.appendBlock(block)
|
||||
continue
|
||||
}
|
||||
var suggestions []string
|
||||
for _, blockS := range schema.Blocks {
|
||||
suggestions = append(suggestions, blockS.Type)
|
||||
}
|
||||
suggestion := nameSuggestion(blockTy, suggestions)
|
||||
if suggestion != "" {
|
||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
||||
} else {
|
||||
// Is there an attribute of the same name?
|
||||
for _, attrS := range schema.Attributes {
|
||||
if attrS.Name == blockTy {
|
||||
suggestion = fmt.Sprintf(" Did you mean to define argument %q?", blockTy)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unsupported block type",
|
||||
Detail: fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
|
||||
Subject: &block.TypeRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(block.Labels) != len(blockS.LabelNames) {
|
||||
if len(blockS.LabelNames) == 0 {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Extraneous label for %s", blockTy),
|
||||
Detail: fmt.Sprintf(
|
||||
"No labels are expected for %s blocks.", blockTy,
|
||||
),
|
||||
Subject: &block.DefRange,
|
||||
Context: &block.DefRange,
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Wrong label count for %s", blockTy),
|
||||
Detail: fmt.Sprintf(
|
||||
"%s blocks expect %d label(s), but got %d.",
|
||||
blockTy, len(blockS.LabelNames), len(block.Labels),
|
||||
),
|
||||
Subject: &block.DefRange,
|
||||
Context: &block.DefRange,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
blocks = append(blocks, block.asHCLBlock())
|
||||
}
|
||||
|
||||
return &hcl.BodyContent{
|
||||
Attributes: attrs,
|
||||
Blocks: blocks,
|
||||
MissingItemRange: b.MissingItemRange_,
|
||||
}, diags
|
||||
}
|
||||
|
||||
// JustAttributes is an implementation of the method of the same name on hcl.Body.
|
||||
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
if len(b.ChildBlocks) > 0 {
|
||||
for _, block := range b.ChildBlocks {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Unexpected %s block", block.Type),
|
||||
Detail: "Blocks are not allowed here.",
|
||||
Context: &block.TypeRange,
|
||||
})
|
||||
}
|
||||
// We'll continue processing anyway, and return any attributes we find
|
||||
// so that the caller can do careful partial analysis.
|
||||
}
|
||||
|
||||
if len(b.Attributes) == 0 {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ret := make(hcl.Attributes, len(b.Attributes))
|
||||
for n, a := range b.Attributes {
|
||||
ret[n] = a.asHCLAttribute(n)
|
||||
}
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
// MissingItemRange is an implementation of the method of the same name on hcl.Body.
|
||||
func (b *Body) MissingItemRange() hcl.Range {
|
||||
return b.MissingItemRange_
|
||||
}
|
||||
|
||||
func (b *Body) setAttribute(name string, attr Attribute) {
|
||||
if b.Attributes == nil {
|
||||
b.Attributes = make(map[string]Attribute)
|
||||
}
|
||||
b.Attributes[name] = attr
|
||||
}
|
||||
|
||||
func (b *Body) appendBlock(block Block) {
|
||||
b.ChildBlocks = append(b.ChildBlocks, block)
|
||||
}
|
||||
|
||||
func (b *Body) addRanges(rngs map[hcl.Range]struct{}) {
|
||||
rngs[b.MissingItemRange_] = struct{}{}
|
||||
for _, attr := range b.Attributes {
|
||||
attr.addRanges(rngs)
|
||||
}
|
||||
for _, block := range b.ChildBlocks {
|
||||
block.addRanges(rngs)
|
||||
}
|
||||
}
|
||||
|
||||
// Block represents a nested block within a body.
|
||||
type Block struct {
|
||||
Type string
|
||||
Labels []string
|
||||
Body Body
|
||||
|
||||
DefRange, TypeRange hcl.Range
|
||||
LabelRanges []hcl.Range
|
||||
}
|
||||
|
||||
func (b *Block) asHCLBlock() *hcl.Block {
|
||||
return &hcl.Block{
|
||||
Type: b.Type,
|
||||
Labels: b.Labels,
|
||||
Body: &b.Body,
|
||||
|
||||
TypeRange: b.TypeRange,
|
||||
DefRange: b.DefRange,
|
||||
LabelRanges: b.LabelRanges,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Block) addRanges(rngs map[hcl.Range]struct{}) {
|
||||
rngs[b.DefRange] = struct{}{}
|
||||
rngs[b.TypeRange] = struct{}{}
|
||||
for _, rng := range b.LabelRanges {
|
||||
rngs[rng] = struct{}{}
|
||||
}
|
||||
b.Body.addRanges(rngs)
|
||||
}
|
||||
|
||||
// Attribute represents an attribute definition within a body.
|
||||
type Attribute struct {
|
||||
Expr Expression
|
||||
|
||||
Range, NameRange hcl.Range
|
||||
}
|
||||
|
||||
func (a *Attribute) asHCLAttribute(name string) *hcl.Attribute {
|
||||
return &hcl.Attribute{
|
||||
Name: name,
|
||||
Expr: &a.Expr,
|
||||
Range: a.Range,
|
||||
NameRange: a.NameRange,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Attribute) addRanges(rngs map[hcl.Range]struct{}) {
|
||||
rngs[a.Range] = struct{}{}
|
||||
rngs[a.NameRange] = struct{}{}
|
||||
a.Expr.addRanges(rngs)
|
||||
}
|
93
hclpack/structure_test.go
Normal file
93
hclpack/structure_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func TestBodyContent(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Body *Body
|
||||
Schema *hcl.BodySchema
|
||||
Want *hcl.BodyContent
|
||||
}{
|
||||
"empty": {
|
||||
&Body{},
|
||||
&hcl.BodySchema{},
|
||||
&hcl.BodyContent{},
|
||||
},
|
||||
"nil": {
|
||||
nil,
|
||||
&hcl.BodySchema{},
|
||||
&hcl.BodyContent{},
|
||||
},
|
||||
"attribute": {
|
||||
&Body{
|
||||
Attributes: map[string]Attribute{
|
||||
"foo": {
|
||||
Expr: Expression{
|
||||
Source: []byte(`"hello"`),
|
||||
SourceType: ExprNative,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{Name: "foo", Required: true},
|
||||
{Name: "bar", Required: false},
|
||||
},
|
||||
},
|
||||
&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"foo": {
|
||||
Name: "foo",
|
||||
Expr: &Expression{
|
||||
Source: []byte(`"hello"`),
|
||||
SourceType: ExprNative,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"block": {
|
||||
&Body{
|
||||
ChildBlocks: []Block{
|
||||
{
|
||||
Type: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: "foo"},
|
||||
},
|
||||
},
|
||||
&hcl.BodyContent{
|
||||
Blocks: hcl.Blocks{
|
||||
{
|
||||
Type: "foo",
|
||||
Body: &Body{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := test.Body.Content(test.Schema)
|
||||
for _, diag := range diags {
|
||||
t.Errorf("unexpected diagnostic: %s", diag.Error())
|
||||
}
|
||||
|
||||
if !cmp.Equal(test.Want, got) {
|
||||
t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
50
hclpack/vlq.go
Normal file
50
hclpack/vlq.go
Normal file
@ -0,0 +1,50 @@
|
||||
package hclpack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/bsm/go-vlq"
|
||||
)
|
||||
|
||||
type vlqBuf []byte
|
||||
|
||||
var vlqSpace [vlq.MaxLen64]byte
|
||||
|
||||
func newVLQBuf(byteCap int) vlqBuf {
|
||||
return make(vlqBuf, 0, byteCap)
|
||||
}
|
||||
|
||||
func (b vlqBuf) AppendInt(i int) vlqBuf {
|
||||
spc := cap(b) - len(b)
|
||||
if spc < len(vlqSpace) {
|
||||
b = append(b, vlqSpace[:]...)
|
||||
b = b[:len(b)-len(vlqSpace)]
|
||||
}
|
||||
into := b[len(b):cap(b)]
|
||||
l := vlq.PutInt(into, int64(i))
|
||||
b = b[:len(b)+l]
|
||||
return b
|
||||
}
|
||||
|
||||
func (b vlqBuf) ReadInt() (int, vlqBuf, error) {
|
||||
v, adv := vlq.Int([]byte(b))
|
||||
if adv <= 0 {
|
||||
if adv == 0 {
|
||||
return 0, b, errors.New("missing expected VLQ value")
|
||||
} else {
|
||||
return 0, b, errors.New("invalid VLQ value")
|
||||
}
|
||||
}
|
||||
if int64(int(v)) != v {
|
||||
return 0, b, errors.New("VLQ value too big for integer on this platform")
|
||||
}
|
||||
return int(v), b[adv:], nil
|
||||
}
|
||||
|
||||
func (b vlqBuf) AppendRawByte(by byte) vlqBuf {
|
||||
return append(b, by)
|
||||
}
|
||||
|
||||
func (b vlqBuf) Bytes() []byte {
|
||||
return []byte(b)
|
||||
}
|
Loading…
Reference in New Issue
Block a user