dcefc5ca24
This allows us to round-trip Body to JSON and back without any loss as long as the expression source codes are always valid UTF-8, and we require that during expression parsing anyway so that is a fine restriction. The JSON encoding is a little noisy to read due to the extra annotations required to be lossless (including source ranges) but still relatively compact due to the base64-VLQ encoding of the source location information.
212 lines
5.4 KiB
Go
212 lines
5.4 KiB
Go
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
|
|
}
|