1package objx
2
3import (
4	"encoding/base64"
5	"encoding/json"
6	"errors"
7	"io/ioutil"
8	"net/url"
9	"strings"
10)
11
12// MSIConvertable is an interface that defines methods for converting your
13// custom types to a map[string]interface{} representation.
14type MSIConvertable interface {
15	// MSI gets a map[string]interface{} (msi) representing the
16	// object.
17	MSI() map[string]interface{}
18}
19
20// Map provides extended functionality for working with
21// untyped data, in particular map[string]interface (msi).
22type Map map[string]interface{}
23
24// Value returns the internal value instance
25func (m Map) Value() *Value {
26	return &Value{data: m}
27}
28
29// Nil represents a nil Map.
30var Nil = New(nil)
31
32// New creates a new Map containing the map[string]interface{} in the data argument.
33// If the data argument is not a map[string]interface, New attempts to call the
34// MSI() method on the MSIConvertable interface to create one.
35func New(data interface{}) Map {
36	if _, ok := data.(map[string]interface{}); !ok {
37		if converter, ok := data.(MSIConvertable); ok {
38			data = converter.MSI()
39		} else {
40			return nil
41		}
42	}
43	return Map(data.(map[string]interface{}))
44}
45
46// MSI creates a map[string]interface{} and puts it inside a new Map.
47//
48// The arguments follow a key, value pattern.
49//
50//
51// Returns nil if any key argument is non-string or if there are an odd number of arguments.
52//
53// Example
54//
55// To easily create Maps:
56//
57//     m := objx.MSI("name", "Mat", "age", 29, "subobj", objx.MSI("active", true))
58//
59//     // creates an Map equivalent to
60//     m := objx.Map{"name": "Mat", "age": 29, "subobj": objx.Map{"active": true}}
61func MSI(keyAndValuePairs ...interface{}) Map {
62	newMap := Map{}
63	keyAndValuePairsLen := len(keyAndValuePairs)
64	if keyAndValuePairsLen%2 != 0 {
65		return nil
66	}
67	for i := 0; i < keyAndValuePairsLen; i = i + 2 {
68		key := keyAndValuePairs[i]
69		value := keyAndValuePairs[i+1]
70
71		// make sure the key is a string
72		keyString, keyStringOK := key.(string)
73		if !keyStringOK {
74			return nil
75		}
76		newMap[keyString] = value
77	}
78	return newMap
79}
80
81// ****** Conversion Constructors
82
83// MustFromJSON creates a new Map containing the data specified in the
84// jsonString.
85//
86// Panics if the JSON is invalid.
87func MustFromJSON(jsonString string) Map {
88	o, err := FromJSON(jsonString)
89	if err != nil {
90		panic("objx: MustFromJSON failed with error: " + err.Error())
91	}
92	return o
93}
94
95// FromJSON creates a new Map containing the data specified in the
96// jsonString.
97//
98// Returns an error if the JSON is invalid.
99func FromJSON(jsonString string) (Map, error) {
100	var m Map
101	err := json.Unmarshal([]byte(jsonString), &m)
102	if err != nil {
103		return Nil, err
104	}
105	m.tryConvertFloat64()
106	return m, nil
107}
108
109func (m Map) tryConvertFloat64() {
110	for k, v := range m {
111		switch v.(type) {
112		case float64:
113			f := v.(float64)
114			if float64(int(f)) == f {
115				m[k] = int(f)
116			}
117		case map[string]interface{}:
118			t := New(v)
119			t.tryConvertFloat64()
120			m[k] = t
121		case []interface{}:
122			m[k] = tryConvertFloat64InSlice(v.([]interface{}))
123		}
124	}
125}
126
127func tryConvertFloat64InSlice(s []interface{}) []interface{} {
128	for k, v := range s {
129		switch v.(type) {
130		case float64:
131			f := v.(float64)
132			if float64(int(f)) == f {
133				s[k] = int(f)
134			}
135		case map[string]interface{}:
136			t := New(v)
137			t.tryConvertFloat64()
138			s[k] = t
139		case []interface{}:
140			s[k] = tryConvertFloat64InSlice(v.([]interface{}))
141		}
142	}
143	return s
144}
145
146// FromBase64 creates a new Obj containing the data specified
147// in the Base64 string.
148//
149// The string is an encoded JSON string returned by Base64
150func FromBase64(base64String string) (Map, error) {
151	decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64String))
152	decoded, err := ioutil.ReadAll(decoder)
153	if err != nil {
154		return nil, err
155	}
156	return FromJSON(string(decoded))
157}
158
159// MustFromBase64 creates a new Obj containing the data specified
160// in the Base64 string and panics if there is an error.
161//
162// The string is an encoded JSON string returned by Base64
163func MustFromBase64(base64String string) Map {
164	result, err := FromBase64(base64String)
165	if err != nil {
166		panic("objx: MustFromBase64 failed with error: " + err.Error())
167	}
168	return result
169}
170
171// FromSignedBase64 creates a new Obj containing the data specified
172// in the Base64 string.
173//
174// The string is an encoded JSON string returned by SignedBase64
175func FromSignedBase64(base64String, key string) (Map, error) {
176	parts := strings.Split(base64String, SignatureSeparator)
177	if len(parts) != 2 {
178		return nil, errors.New("objx: Signed base64 string is malformed")
179	}
180
181	sig := HashWithKey(parts[0], key)
182	if parts[1] != sig {
183		return nil, errors.New("objx: Signature for base64 data does not match")
184	}
185	return FromBase64(parts[0])
186}
187
188// MustFromSignedBase64 creates a new Obj containing the data specified
189// in the Base64 string and panics if there is an error.
190//
191// The string is an encoded JSON string returned by Base64
192func MustFromSignedBase64(base64String, key string) Map {
193	result, err := FromSignedBase64(base64String, key)
194	if err != nil {
195		panic("objx: MustFromSignedBase64 failed with error: " + err.Error())
196	}
197	return result
198}
199
200// FromURLQuery generates a new Obj by parsing the specified
201// query.
202//
203// For queries with multiple values, the first value is selected.
204func FromURLQuery(query string) (Map, error) {
205	vals, err := url.ParseQuery(query)
206	if err != nil {
207		return nil, err
208	}
209	m := Map{}
210	for k, vals := range vals {
211		m[k] = vals[0]
212	}
213	return m, nil
214}
215
216// MustFromURLQuery generates a new Obj by parsing the specified
217// query.
218//
219// For queries with multiple values, the first value is selected.
220//
221// Panics if it encounters an error
222func MustFromURLQuery(query string) Map {
223	o, err := FromURLQuery(query)
224	if err != nil {
225		panic("objx: MustFromURLQuery failed with error: " + err.Error())
226	}
227	return o
228}
229