package json import ( "bytes" "encoding/json" "sort" "github.com/zclconf/go-cty/cty" ) func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error { if val.IsMarked() { return path.NewErrorf("value has marks, so it cannot be serialized as JSON") } // If we're going to decode as DynamicPseudoType then we need to save // dynamic type information to recover the real type. if t == cty.DynamicPseudoType && val.Type() != cty.DynamicPseudoType { return marshalDynamic(val, path, b) } if val.IsNull() { b.WriteString("null") return nil } if !val.IsKnown() { return path.NewErrorf("value is not known") } // The caller should've guaranteed that the given val is conformant with // the given type t, so we'll proceed under that assumption here. switch { case t.IsPrimitiveType(): switch t { case cty.String: json, err := json.Marshal(val.AsString()) if err != nil { return path.NewErrorf("failed to serialize value: %s", err) } b.Write(json) return nil case cty.Number: if val.RawEquals(cty.PositiveInfinity) || val.RawEquals(cty.NegativeInfinity) { return path.NewErrorf("cannot serialize infinity as JSON") } b.WriteString(val.AsBigFloat().Text('f', -1)) return nil case cty.Bool: if val.True() { b.WriteString("true") } else { b.WriteString("false") } return nil default: panic("unsupported primitive type") } case t.IsListType(), t.IsSetType(): b.WriteRune('[') first := true ety := t.ElementType() it := val.ElementIterator() path := append(path, nil) // local override of 'path' with extra element for it.Next() { if !first { b.WriteRune(',') } ek, ev := it.Element() path[len(path)-1] = cty.IndexStep{ Key: ek, } err := marshal(ev, ety, path, b) if err != nil { return err } first = false } b.WriteRune(']') return nil case t.IsMapType(): b.WriteRune('{') first := true ety := t.ElementType() it := val.ElementIterator() path := append(path, nil) // local override of 'path' with extra element for it.Next() { if !first { b.WriteRune(',') } ek, ev := it.Element() path[len(path)-1] = cty.IndexStep{ Key: ek, } var err error err = marshal(ek, ek.Type(), path, b) if err != nil { return err } b.WriteRune(':') err = marshal(ev, ety, path, b) if err != nil { return err } first = false } b.WriteRune('}') return nil case t.IsTupleType(): b.WriteRune('[') etys := t.TupleElementTypes() it := val.ElementIterator() path := append(path, nil) // local override of 'path' with extra element i := 0 for it.Next() { if i > 0 { b.WriteRune(',') } ety := etys[i] ek, ev := it.Element() path[len(path)-1] = cty.IndexStep{ Key: ek, } err := marshal(ev, ety, path, b) if err != nil { return err } i++ } b.WriteRune(']') return nil case t.IsObjectType(): b.WriteRune('{') atys := t.AttributeTypes() path := append(path, nil) // local override of 'path' with extra element names := make([]string, 0, len(atys)) for k := range atys { names = append(names, k) } sort.Strings(names) for i, k := range names { aty := atys[k] if i > 0 { b.WriteRune(',') } av := val.GetAttr(k) path[len(path)-1] = cty.GetAttrStep{ Name: k, } var err error err = marshal(cty.StringVal(k), cty.String, path, b) if err != nil { return err } b.WriteRune(':') err = marshal(av, aty, path, b) if err != nil { return err } } b.WriteRune('}') return nil case t.IsCapsuleType(): rawVal := val.EncapsulatedValue() jsonVal, err := json.Marshal(rawVal) if err != nil { return path.NewError(err) } b.Write(jsonVal) return nil default: // should never happen return path.NewErrorf("cannot JSON-serialize %s", t.FriendlyName()) } } // marshalDynamic adds an extra wrapping object containing dynamic type // information for the given value. func marshalDynamic(val cty.Value, path cty.Path, b *bytes.Buffer) error { typeJSON, err := MarshalType(val.Type()) if err != nil { return path.NewErrorf("failed to serialize type: %s", err) } b.WriteString(`{"value":`) marshal(val, val.Type(), path, b) b.WriteString(`,"type":`) b.Write(typeJSON) b.WriteRune('}') return nil }