1// Copyright 2013 Matthew Baird
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//     http://www.apache.org/licenses/LICENSE-2.0
6// Unless required by applicable law or agreed to in writing, software
7// distributed under the License is distributed on an "AS IS" BASIS,
8// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9// See the License for the specific language governing permissions and
10// limitations under the License.
11
12package elastigo
13
14import (
15	"encoding/json"
16	"io/ioutil"
17	"net/http"
18	"net/http/httptest"
19	"net/url"
20	"reflect"
21	"sort"
22	"strings"
23	"testing"
24)
25
26var (
27	mux    *http.ServeMux
28	server *httptest.Server
29)
30
31func setup(t *testing.T) *Conn {
32	mux = http.NewServeMux()
33	server = httptest.NewServer(mux)
34	c := NewTestConn()
35
36	serverURL, err := url.Parse(server.URL)
37	if err != nil {
38		t.Fatalf("Error: %v", err)
39	}
40
41	c.Domain = strings.Split(serverURL.Host, ":")[0]
42	c.Port = strings.Split(serverURL.Host, ":")[1]
43
44	return c
45}
46
47func teardown() {
48	server.Close()
49}
50
51type TestStruct struct {
52	Id            string `json:"id" elastic:"index:not_analyzed"`
53	DontIndex     string `json:"dontIndex" elastic:"index:no"`
54	Number        int    `json:"number" elastic:"type:integer,index:analyzed"`
55	Omitted       string `json:"-"`
56	NoJson        string `elastic:"type:string"`
57	unexported    string
58	JsonOmitEmpty string `json:"jsonOmitEmpty,omitempty" elastic:"type:string"`
59	Embedded
60	Inner        InnerStruct   `json:"inner"`
61	InnerP       *InnerStruct  `json:"pointer_to_inner"`
62	InnerS       []InnerStruct `json:"slice_of_inner"`
63	MultiAnalyze string        `json:"multi_analyze"`
64	NestedObject NestedStruct  `json:"nestedObject" elastic:"type:nested"`
65}
66
67type Embedded struct {
68	EmbeddedField string `json:"embeddedField" elastic:"type:string"`
69}
70
71type InnerStruct struct {
72	InnerField string `json:"innerField" elastic:"type:date"`
73}
74
75type NestedStruct struct {
76	InnerField string `json:"innerField" elastic:"type:date"`
77}
78
79// Sorting string
80// RuneSlice implements sort.Interface (http://golang.org/pkg/sort/#Interface)
81type RuneSlice []rune
82
83func (p RuneSlice) Len() int           { return len(p) }
84func (p RuneSlice) Less(i, j int) bool { return p[i] < p[j] }
85func (p RuneSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
86
87// sorted func returns string with sorted characters
88func sorted(s string) string {
89	runes := []rune(s)
90	sort.Sort(RuneSlice(runes))
91	return string(runes)
92}
93
94func TestPutMapping(t *testing.T) {
95	c := setup(t)
96	defer teardown()
97
98	options := MappingOptions{
99		Timestamp: TimestampOptions{Enabled: true},
100		Id:        IdOptions{Index: "analyzed", Path: "id"},
101		Parent:    &ParentOptions{Type: "testParent"},
102		TTL:       &TTLOptions{Enabled: true, Default: "1w"},
103		Properties: map[string]interface{}{
104			// special properties that can't be expressed as tags
105			"multi_analyze": map[string]interface{}{
106				"type": "multi_field",
107				"fields": map[string]map[string]string{
108					"ma_analyzed":    {"type": "string", "index": "analyzed"},
109					"ma_notanalyzed": {"type": "string", "index": "not_analyzed"},
110				},
111			},
112		},
113		DynamicTemplates: []map[string]interface{}{
114			{
115				"strings": map[string]interface{}{
116					"match_mapping_type": "string",
117					"mapping": map[string]interface{}{
118						"type":  "string",
119						"index": "not_analyzed",
120					},
121				},
122			},
123		},
124	}
125
126	expValue := MappingForType("myType", MappingOptions{
127		Timestamp: TimestampOptions{Enabled: true},
128		Id:        IdOptions{Index: "analyzed", Path: "id"},
129		Parent:    &ParentOptions{Type: "testParent"},
130		TTL:       &TTLOptions{Enabled: true, Default: "1w"},
131		Properties: map[string]interface{}{
132			"NoJson":        map[string]string{"type": "string"},
133			"dontIndex":     map[string]string{"index": "no"},
134			"embeddedField": map[string]string{"type": "string"},
135			"id":            map[string]string{"index": "not_analyzed"},
136			"jsonOmitEmpty": map[string]string{"type": "string"},
137			"number":        map[string]string{"index": "analyzed", "type": "integer"},
138			"multi_analyze": map[string]interface{}{
139				"type": "multi_field",
140				"fields": map[string]map[string]string{
141					"ma_analyzed":    {"type": "string", "index": "analyzed"},
142					"ma_notanalyzed": {"type": "string", "index": "not_analyzed"},
143				},
144			},
145			"inner": map[string]map[string]map[string]string{
146				"properties": {
147					"innerField": {"type": "date"},
148				},
149			},
150			"pointer_to_inner": map[string]map[string]map[string]string{
151				"properties": {
152					"innerField": {"type": "date"},
153				},
154			},
155			"slice_of_inner": map[string]map[string]map[string]string{
156				"properties": {
157					"innerField": {"type": "date"},
158				},
159			},
160			"nestedObject": map[string]interface{}{
161				"type": "nested",
162				"properties": map[string]map[string]string{
163					"innerField": {"type": "date"},
164				},
165			},
166		},
167		DynamicTemplates: []map[string]interface{}{
168			{
169				"strings": map[string]interface{}{
170					"match_mapping_type": "string",
171					"mapping": map[string]interface{}{
172						"type":  "string",
173						"index": "not_analyzed",
174					},
175				},
176			},
177		},
178	})
179
180	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
181		var value map[string]interface{}
182		bd, err := ioutil.ReadAll(r.Body)
183		json.NewDecoder(strings.NewReader(string(bd))).Decode(&value)
184		expValJson, err := json.MarshalIndent(expValue, "", "  ")
185		if err != nil {
186			t.Errorf("Got error: %v", err)
187		}
188		valJson, err := json.MarshalIndent(value, "", "  ")
189		if err != nil {
190			t.Errorf("Got error: %v", err)
191		}
192
193		if sorted(string(expValJson)) != sorted(string(valJson)) {
194			t.Errorf("Expected %s but got %s", string(expValJson), string(valJson))
195		}
196	})
197
198	err := c.PutMapping("myIndex", "myType", TestStruct{}, options)
199	if err != nil {
200		t.Errorf("Error: %v", err)
201	}
202}
203
204func TestPutMappingFromJSON(t *testing.T) {
205	c := setup(t)
206	defer teardown()
207	/*
208		options := MappingOptions{
209			Timestamp: TimestampOptions{Enabled: true},
210			Id:        IdOptions{Index: "analyzed", Path: "id"},
211			Parent:    &ParentOptions{Type: "testParent"},
212			Properties: map[string]interface{}{
213				// special properties that can't be expressed as tags
214				"multi_analyze": map[string]interface{}{
215					"type": "multi_field",
216					"fields": map[string]map[string]string{
217						"ma_analyzed":    {"type": "string", "index": "analyzed"},
218						"ma_notanalyzed": {"type": "string", "index": "not_analyzed"},
219					},
220				},
221			},
222			DynamicTemplates: []map[string]interface{}{
223				"strings": map[string]interface{}{
224					"match_mapping_type": "string",
225					"mapping": {
226						"type":  "string",
227						"index": "not_analyzed",
228					},
229				},
230			},
231		}
232	*/
233
234	options := `{
235					"myType": {
236						"_id": {
237							"index": "analyzed",
238							"path": "id"
239						},
240						"_timestamp": {
241							"enabled": true
242						},
243						"_parent": {
244							"type": "testParent"
245						},
246						"properties": {
247							"analyzed_string": {
248								"type": "string",
249								"index": "analyzed"
250							},
251							"multi_analyze": {
252								"type": "multi_field",
253								"fields": {
254									"ma_analyzed":    {
255										"type": "string",
256										"index": "analyzed"
257									},
258									"ma_notanalyzed": {
259										"type": "string",
260										"index": "not_analyzed"
261									}
262								}
263							}
264						},
265						"dynamic_templates": [
266							{
267								"strings": {
268									"match_mapping_type": "string",
269									"mapping": {
270										"type": "string",
271										"index": "not_analyzed"
272									}
273								}
274							}
275						]
276					}
277				}`
278
279	expValue := map[string]interface{}{
280		"myType": map[string]interface{}{
281			"_timestamp": map[string]interface{}{
282				"enabled": true,
283			},
284			"_id": map[string]interface{}{
285				"index": "analyzed",
286				"path":  "id",
287			},
288			"_parent": map[string]interface{}{
289				"type": "testParent",
290			},
291			"properties": map[string]interface{}{
292				"analyzed_string": map[string]string{
293					"type":  "string",
294					"index": "analyzed",
295				},
296				"multi_analyze": map[string]interface{}{
297					"type": "multi_field",
298					"fields": map[string]map[string]string{
299						"ma_analyzed":    {"type": "string", "index": "analyzed"},
300						"ma_notanalyzed": {"type": "string", "index": "not_analyzed"},
301					},
302				},
303			},
304			"dynamic_templates": []map[string]interface{}{
305				{
306					"strings": map[string]interface{}{
307						"match_mapping_type": "string",
308						"mapping": map[string]interface{}{
309							"type":  "string",
310							"index": "not_analyzed",
311						},
312					},
313				},
314			},
315		},
316	}
317
318	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
319		var value map[string]interface{}
320		bd, err := ioutil.ReadAll(r.Body)
321		err = json.Unmarshal(bd, &value)
322		if err != nil {
323			t.Errorf("Got error: %v", err)
324		}
325		expValJson, err := json.MarshalIndent(expValue, "", "  ")
326		if err != nil {
327			t.Errorf("Got error: %v", err)
328		}
329
330		valJson, err := json.MarshalIndent(value, "", "  ")
331		if err != nil {
332			t.Errorf("Got error: %v", err)
333		}
334
335		if sorted(string(expValJson)) != sorted(string(valJson)) {
336			t.Errorf("Expected %s but got %s", string(expValJson), string(valJson))
337		}
338	})
339
340	err := c.PutMappingFromJSON("myIndex", "myType", []byte(options))
341	if err != nil {
342		t.Errorf("Error: %v", err)
343	}
344}
345
346type StructWithEmptyElasticTag struct {
347	Field string `json:"field" elastic:""`
348}
349
350func TestPutMapping_empty_elastic_tag_is_accepted(t *testing.T) {
351	properties := map[string]interface{}{}
352	getProperties(reflect.TypeOf(StructWithEmptyElasticTag{}), properties)
353	if len(properties) != 0 {
354		t.Errorf("Expected empty properites but got: %v", properties)
355	}
356}
357