diff --git a/conversions.go b/conversions.go index 01c63d7..e84efa6 100644 --- a/conversions.go +++ b/conversions.go @@ -8,6 +8,8 @@ import ( "fmt" "net/url" "strconv" + + "gopkg.in/yaml.v3" ) // SignatureSeparator is the character that is used to @@ -99,16 +101,28 @@ func cleanUpMapArray(in []Map) []Map { return result } +func cleanUpMIIArray(in []map[interface{}]interface{}) []Map { + result := make([]Map, len(in)) + for i, v := range in { + result[i] = cleanUpInterfaceMap(v) + } + return result +} + func cleanUp(v interface{}) interface{} { switch v := v.(type) { case []interface{}: return cleanUpInterfaceArray(v) case []map[string]interface{}: return cleanUpMSIArray(v) + case []map[interface{}]interface{}: + return cleanUpMIIArray(v) case map[interface{}]interface{}: return cleanUpInterfaceMap(v) case Map: return cleanUpStringMap(v) + case map[string]interface{}: + return cleanUpStringMap(v) case []Map: return cleanUpMapArray(v) default: @@ -126,6 +140,48 @@ func (m Map) MustJSON() string { return result } +// YAML converts the contained object to a YAML string +// representation +func (m Map) YAML() (resultStr string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("objx: YAML encode failed with: %v", r) + } + }() + + for k, v := range m { + m[k] = cleanUp(v) + } + + result, err := yaml.Marshal(m) + if err != nil { + err = errors.New("objx: YAML encode failed with: " + err.Error()) + } + return string(result), err +} + +// MustYAML converts the contained object to a YAML string +// representation and panics if there is an error +func (m Map) MustYAML() string { + result, err := m.YAML() + if err != nil { + panic(err.Error()) + } + return result +} + +// Yaml converts the contained object to a YAML string +// representation. Alias for YAML. +func (m Map) Yaml() (string, error) { + return m.YAML() +} + +// MustYaml converts the contained object to a YAML string +// representation and panics if there is an error. Alias for MustYAML. +func (m Map) MustYaml() string { + return m.MustYAML() +} + // Base64 converts the contained object to a Base64 string // representation of the JSON string representation func (m Map) Base64() (string, error) { diff --git a/conversions_test.go b/conversions_test.go index d359d1e..01c91d4 100644 --- a/conversions_test.go +++ b/conversions_test.go @@ -45,6 +45,65 @@ func TestConversionJSONWithError(t *testing.T) { assert.Error(t, err) } +func TestConversionYAML(t *testing.T) { + o := objx.Map{"name": "Mat", "age": 30} + + result, err := o.YAML() + require.NoError(t, err) + require.NotNil(t, result) + + parsed, err := objx.FromYAML(result) + require.NoError(t, err) + assert.Equal(t, "Mat", parsed.Get("name").Str()) + assert.Equal(t, 30, parsed.Get("age").Int()) + + assert.Equal(t, result, o.MustYAML()) + assert.Equal(t, result, o.MustYaml()) + + aliasResult, err := o.Yaml() + require.NoError(t, err) + assert.Equal(t, result, aliasResult) + + i := objx.Map{ + "a": map[interface{}]interface{}{"b": objx.Map{"c": map[interface{}]interface{}{"d": "e"}}, + "f": []objx.Map{{"g": map[interface{}]interface{}{"h": "i"}}}, + "j": []map[string]interface{}{{"k": map[interface{}]interface{}{"l": "m"}}}, + "n": []interface{}{objx.Map{"o": "p"}}, + }, + } + + result, err = i.YAML() + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, result, i.MustYAML()) + + parsedNested, err := objx.FromYAML(result) + require.NoError(t, err) + assert.Equal(t, "e", parsedNested.Get("a.b.c.d").Str()) + assert.Equal(t, "i", parsedNested.Get("a.f[0].g.h").Str()) + assert.Equal(t, "m", parsedNested.Get("a.j[0].k.l").Str()) + assert.Equal(t, "p", parsedNested.Get("a.n[0].o").Str()) +} + +func TestConversionYAMLWithError(t *testing.T) { + o := objx.MSI() + o["test"] = func() {} + + assert.Panics(t, func() { + o.MustYAML() + }) + + assert.Panics(t, func() { + o.MustYaml() + }) + + _, err := o.YAML() + assert.Error(t, err) + + _, err = o.Yaml() + assert.Error(t, err) +} + func TestConversionBase64(t *testing.T) { o := objx.Map{"name": "Mat"} diff --git a/go.mod b/go.mod index 6817ec2..18e0421 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/stretchr/objx go 1.20 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index e69de29..a62c313 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/map.go b/map.go index ab9f9ae..2fc11e2 100644 --- a/map.go +++ b/map.go @@ -4,9 +4,12 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "io/ioutil" "net/url" "strings" + + "gopkg.in/yaml.v3" ) // MSIConvertable is an interface that defines methods for converting your @@ -129,6 +132,97 @@ func FromJSONSlice(jsonString string) ([]Map, error) { return slice, nil } +// MustFromYAML creates a new Map containing the data specified in the +// yamlString. +// +// Panics if the YAML is invalid. +func MustFromYAML(yamlString string) Map { + o, err := FromYAML(yamlString) + if err != nil { + panic("objx: MustFromYAML failed with error: " + err.Error()) + } + return o +} + +// MustFromYaml is an alias for MustFromYAML. +func MustFromYaml(yamlString string) Map { + return MustFromYAML(yamlString) +} + +// MustFromYAMLSlice creates a new slice of Map containing the data specified in the +// yamlString. Works with YAML with a top level array. +// +// Panics if the YAML is invalid. +func MustFromYAMLSlice(yamlString string) []Map { + slice, err := FromYAMLSlice(yamlString) + if err != nil { + panic("objx: MustFromYAMLSlice failed with error: " + err.Error()) + } + return slice +} + +// MustFromYamlSlice is an alias for MustFromYAMLSlice. +func MustFromYamlSlice(yamlString string) []Map { + return MustFromYAMLSlice(yamlString) +} + +// FromYAML creates a new Map containing the data specified in the +// yamlString. +// +// Returns an error if the YAML is invalid. +func FromYAML(yamlString string) (m Map, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("objx: YAML decode failed with: %v", r) + m = Nil + } + }() + + err = yaml.Unmarshal([]byte(yamlString), &m) + if err != nil { + return Nil, err + } + for k, v := range m { + m[k] = cleanUp(v) + } + return m, nil +} + +// FromYaml is an alias for FromYAML. +func FromYaml(yamlString string) (Map, error) { + return FromYAML(yamlString) +} + +// FromYAMLSlice creates a new slice of Map containing the data specified in the +// yamlString. Works with YAML with a top level array. +// +// Returns an error if the YAML is invalid. +func FromYAMLSlice(yamlString string) (slice []Map, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("objx: YAML decode failed with: %v", r) + slice = nil + } + }() + + err = yaml.Unmarshal([]byte(yamlString), &slice) + if err != nil { + return nil, err + } + for i, sm := range slice { + for k, v := range sm { + sm[k] = cleanUp(v) + } + slice[i] = sm + } + return slice, nil +} + +// FromYamlSlice is an alias for FromYAMLSlice. +func FromYamlSlice(yamlString string) ([]Map, error) { + return FromYAMLSlice(yamlString) +} + // FromBase64 creates a new Obj containing the data specified // in the Base64 string. // diff --git a/map_test.go b/map_test.go index 06eb996..560c01b 100644 --- a/map_test.go +++ b/map_test.go @@ -242,3 +242,112 @@ func TestJSONTopLevelSliceWithError(t *testing.T) { _ = objx.MustFromJSONSlice(`{"id": 10000001}`) }) } + +func TestMapFromYAML(t *testing.T) { + yamlString := "name: Mat\nage: 30\n" + o, err := objx.FromYAML(yamlString) + + require.NoError(t, err) + require.NotNil(t, o) + assert.Equal(t, "Mat", o["name"]) + assert.Equal(t, 30, o["age"]) + assert.Equal(t, "Mat", o.Get("name").Str()) + assert.Equal(t, 30, o.Get("age").Int()) + + // Test alias FromYaml + o2, err := objx.FromYaml(yamlString) + require.NoError(t, err) + require.NotNil(t, o2) + assert.Equal(t, "Mat", o2.Get("name").Str()) +} + +func TestMapMustFromYAML(t *testing.T) { + yamlString := "name: Mat\nage: 30\n" + o := objx.MustFromYAML(yamlString) + + require.NotNil(t, o) + assert.Equal(t, "Mat", o.Get("name").Str()) + assert.Equal(t, 30, o.Get("age").Int()) + + // Test alias MustFromYaml + o2 := objx.MustFromYaml(yamlString) + require.NotNil(t, o2) + assert.Equal(t, "Mat", o2.Get("name").Str()) +} + +func TestMapFromYAMLWithError(t *testing.T) { + _, err := objx.FromYAML(":\n invalid: yaml: :") + assert.Error(t, err) + + assert.Panics(t, func() { + objx.MustFromYAML(":\n invalid: yaml: :") + }) + + assert.Panics(t, func() { + objx.MustFromYaml(":\n invalid: yaml: :") + }) +} + +func TestMapFromYAMLNested(t *testing.T) { + yamlString := ` +user: + name: Tyler + address: + city: Salt Lake City + state: UT + tags: + - admin + - developer +` + m, err := objx.FromYAML(yamlString) + require.NoError(t, err) + require.NotNil(t, m) + + assert.Equal(t, "Tyler", m.Get("user.name").Str()) + assert.Equal(t, "Salt Lake City", m.Get("user.address.city").Str()) + assert.Equal(t, "UT", m.Get("user.address.state").Str()) + assert.Equal(t, "admin", m.Get("user.tags[0]").Str()) + assert.Equal(t, "developer", m.Get("user.tags[1]").Str()) +} + +func TestYAMLTopLevelSlice(t *testing.T) { + yamlString := ` +- id: 10000001 + name: Item 1 +- id: 42 + name: Item 2 +` + slice, err := objx.FromYAMLSlice(yamlString) + + assert.NoError(t, err) + require.Len(t, slice, 2) + assert.Equal(t, 10000001, slice[0].Get("id").MustInt()) + assert.Equal(t, "Item 1", slice[0].Get("name").Str()) + assert.Equal(t, 42, slice[1].Get("id").MustInt()) + assert.Equal(t, "Item 2", slice[1].Get("name").Str()) + + mustSlice := objx.MustFromYAMLSlice(yamlString) + require.Len(t, mustSlice, 2) + assert.Equal(t, 10000001, mustSlice[0].Get("id").MustInt()) + + // Test aliases + aliasSlice, err := objx.FromYamlSlice(yamlString) + assert.NoError(t, err) + require.Len(t, aliasSlice, 2) + + mustAliasSlice := objx.MustFromYamlSlice(yamlString) + require.Len(t, mustAliasSlice, 2) +} + +func TestYAMLTopLevelSliceWithError(t *testing.T) { + slice, err := objx.FromYAMLSlice(":\n invalid: yaml: :") + + assert.Error(t, err) + assert.Nil(t, slice) + assert.Panics(t, func() { + _ = objx.MustFromYAMLSlice(":\n invalid: yaml: :") + }) + assert.Panics(t, func() { + _ = objx.MustFromYamlSlice(":\n invalid: yaml: :") + }) +}