-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfj.go
2005 lines (1955 loc) · 69.6 KB
/
fj.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package fj
import (
"io"
"os"
"strconv"
"strings"
"time"
"github.com/sivaosorg/unify4g"
)
// Parse parses a JSON string and returns a Context representing the parsed value.
//
// This function processes the input JSON string and attempts to determine the type of the value it represents.
// It handles objects, arrays, numbers, strings, booleans, and null values. The function does not validate whether
// the JSON is well-formed, and instead returns a Context object that represents the first valid JSON element found
// in the string. Invalid JSON may result in unexpected behavior, so for input from unpredictable sources, consider
// using the `Valid` function first.
//
// Parameters:
// - `json`: A string containing the JSON data to be parsed. This function expects well-formed JSON and does not
// perform comprehensive validation.
//
// Returns:
// - A `Context` that represents the parsed JSON element. The `Context` contains details about the type, value,
// and position of the JSON element, including raw and unprocessed string data.
//
// Notes:
// - The function attempts to determine the type of the JSON element by inspecting the first character in the
// string. It supports the following types: Object (`{`), Array (`[`), Number, String (`"`), Boolean (`true` / `false`),
// and Null (`null`).
// - The function sets the `unprocessed` field of the `Context` to the raw JSON string for further processing, and
// sets the `kind` field to represent the type of the value (e.g., `String`, `Number`, `True`, `False`, `JSON`, `Null`).
//
// Example Usage:
//
// json := "{\"name\": \"John\", \"age\": 30}"
// ctx := Parse(json)
// fmt.Println(ctx.kind) // Output: JSON (if the input starts with '{')
//
// json := "12345"
// ctx := Parse(json)
// fmt.Println(ctx.kind) // Output: Number (if the input is a numeric value)
//
// json := "\"Hello, World!\""
// ctx := Parse(json)
// fmt.Println(ctx.kind) // Output: String (if the input is a string)
//
// Returns:
// - `Context`: The parsed result, which may represent an object, array, string, number, boolean, or null.
func Parse(json string) Context {
var value Context
i := 0
for ; i < len(json); i++ {
if json[i] == '{' || json[i] == '[' {
value.kind = JSON
value.unprocessed = json[i:]
break
}
if json[i] <= ' ' {
continue
}
switch json[i] {
case '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'i', 'I', 'N':
value.kind = Number
value.unprocessed, value.numeric = getNumeric(json[i:])
case 'n':
if i+1 < len(json) && json[i+1] != 'u' {
// nan
value.kind = Number
value.unprocessed, value.numeric = getNumeric(json[i:])
} else {
// null
value.kind = Null
value.unprocessed = lowerPrefix(json[i:])
}
case 't':
value.kind = True
value.unprocessed = lowerPrefix(json[i:])
case 'f':
value.kind = False
value.unprocessed = lowerPrefix(json[i:])
case '"':
value.kind = String
value.unprocessed, value.strings = unescapeJSONEncoded(json[i:])
default:
return Context{}
}
break
}
if value.Exists() {
value.index = i
}
return value
}
// ParseBufio reads a JSON string from an `io.Reader` and parses it into a Context.
//
// This function combines the reading and parsing operations. It first reads the JSON
// string using the `BufioRead` function, then processes the string using the `Parse`
// function to extract details about the first valid JSON element. If reading the JSON
// fails, the returned Context contains the encountered error.
//
// Parameters:
// - in: An `io.Reader` from which the JSON string will be read. This could be a file,
// network connection, or any other source that implements the `io.Reader` interface.
//
// Returns:
// - A `Context` representing the parsed JSON element. If an error occurs during the
// reading process, the `err` field of the `Context` is set with the error, and the
// other fields remain empty.
//
// Example Usage:
//
// // Reading JSON from a file
// file, err := os.Open("example.json")
// if err != nil {
// log.Fatal(err)
// }
// defer file.Close()
//
// ctx := ParseBufio(file)
// if ctx.err != nil {
// log.Fatalf("Failed to parse JSON: %v", ctx.err)
// }
// fmt.Println(ctx.kind)
//
// // Reading JSON from standard input
// fmt.Println("Enter JSON:")
// ctx = ParseBufio(os.Stdin)
// if ctx.err != nil {
// log.Fatalf("Failed to parse JSON: %v", ctx.err)
// }
// fmt.Println(ctx.kind)
//
// Notes:
// - This function is particularly useful when working with JSON data from streams or large files.
// - The `Parse` function is responsible for the actual parsing, while this function ensures the
// JSON string is read correctly before parsing begins.
// - If the input JSON is malformed or invalid, the returned Context from `Parse` will reflect
// the issue as an empty Context or an error in the `err` field.
func ParseBufio(in io.Reader) Context {
json, err := BufioRead(in)
if err != nil {
return Context{
err: err,
}
}
return Parse(json)
}
// ParseFilepath reads a JSON string from a file specified by the filepath and parses it into a Context.
//
// This function opens the specified file, reads its contents using the `ParseBufio` function,
// and returns a Context representing the parsed JSON element. If any error occurs during
// file reading or JSON parsing, the returned Context will include the error details.
//
// Parameters:
// - filepath: The path to the JSON file to be read and parsed.
//
// Returns:
// - A `Context` representing the parsed JSON element. If an error occurs during the
// file reading or parsing process, the `err` field of the returned Context will be set
// with the error, and the other fields will remain empty.
//
// Example Usage:
//
// ctx := ParseFilepath("example.json")
// if ctx.err != nil {
// log.Fatalf("Failed to parse JSON: %v", ctx.err)
// }
// fmt.Println(ctx.String())
//
// Notes:
// - This function is useful for reading and parsing JSON directly from files.
// - The file is opened and closed automatically within this function, and any errors encountered
// are captured in the returned `Context`.
func ParseFilepath(filepath string) Context {
file, err := os.Open(filepath)
if err != nil {
return Context{
err: err,
}
}
defer file.Close()
return ParseBufio(file)
}
// ParseBytes parses a JSON byte slice and returns a Context representing the parsed value.
//
// This function is a wrapper around the `Parse` function, designed specifically for handling JSON data
// in the form of a byte slice. It converts the byte slice into a string and then calls `Parse` to process
// the JSON data. If you're working with raw JSON data as bytes, using this method is preferred over
// manually converting the bytes to a string and passing it to `Parse`.
//
// Parameters:
// - `json`: A byte slice containing the JSON data to be parsed.
//
// Returns:
// - A `Context` representing the parsed JSON element, similar to the behavior of `Parse`. The `Context`
// contains information about the type, value, and position of the JSON element, including the raw and
// unprocessed string data.
//
// Example Usage:
//
// json := []byte("{\"name\": \"Alice\", \"age\": 25}")
// ctx := ParseBytes(json)
// fmt.Println(ctx.kind) // Output: JSON (if the input is an object)
//
// Returns:
// - `Context`: The parsed result, representing the parsed JSON element, such as an object, array, string,
// number, boolean, or null.
func ParseBytes(json []byte) Context {
return Parse(string(json))
}
// Get searches for a specified path within the provided JSON string and returns the corresponding value as a Context.
// The path is provided in dot notation, where each segment represents a key or index. The function supports wildcards
// (`*` and `?`), array indexing, and special characters like '#' to access array lengths or child paths. The function
// will return the first matching result it finds along the specified path.
//
// Path Syntax:
// - Dot notation: "name.last" or "age" for direct key lookups.
// - Wildcards: "*" matches any key, "?" matches a single character.
// - Array indexing: "children.0" accesses the first item in the "children" array.
// - The '#' character returns the number of elements in an array (e.g., "children.#" returns the array length).
// - The dot (`.`) and wildcard characters (`*`, `?`) can be escaped with a backslash (`\`).
//
// Example Usage:
//
// json := `{
// "user": {"firstName": "Alice", "lastName": "Johnson"},
// "age": 29,
// "siblings": ["Ben", "Clara", "David"],
// "friends": [
// {"firstName": "Tom", "lastName": "Smith"},
// {"firstName": "Sophia", "lastName": "Davis"}
// ],
// "address": {"city": "New York", "zipCode": "10001"}
// }`
//
// // Examples of Get function with paths:
// Get(json, "user.lastName") // Returns: "Johnson"
// Get(json, "age") // Returns: 29
// Get(json, "siblings.#") // Returns: 3 (number of siblings)
// Get(json, "siblings.1") // Returns: "Clara" (second sibling)
// Get(json, "friends.#.firstName") // Returns: ["Tom", "Sophia"]
// Get(json, "address.zipCode") // Returns: "10001"
//
// Details:
// - The function does not validate JSON format but expects well-formed input.
// Invalid JSON may result in unexpected behavior.
// - transformers (e.g., `@` for adjusting paths) and special sub-selectors (e.g., `[` and `{`) are supported and processed
// in the path before extracting values.
// - For complex structures, the function analyzes the provided path, handles nested arrays or objects, and returns
// a Context containing the value found at the specified location.
//
// Parameters:
// - `json`: A string containing the JSON data to search through.
// - `path`: A string representing the path to the desired value, using dot notation or other special characters as described.
//
// Returns:
// - `Context`: A Context object containing the value found at the specified path, including information such as the
// type (`kind`), the raw JSON string (`unprocessed`), and the parsed value if available (e.g., `strings` for strings).
//
// Notes:
// - If the path is not found, the returned Context will reflect this with an empty or null value.
func Get(json, path string) Context {
if len(path) > 1 {
if (path[0] == '@' && !DisableTransformers) || path[0] == '!' {
var ok bool
var cPath string
var cJson string
if path[0] == '@' && !DisableTransformers {
cPath, cJson, ok = adjustTransformer(json, path)
} else if path[0] == '!' {
cPath, cJson, ok = parseStaticSegment(path)
}
if ok {
path = cPath
if len(path) > 0 && (path[0] == '|' || path[0] == '.') {
res := Get(cJson, path[1:])
res.index = 0
res.indexes = nil
return res
}
return Parse(cJson)
}
}
if path[0] == '[' || path[0] == '{' {
kind := path[0] // using a sub-selector path
var ok bool
var subs []subSelector
subs, path, ok = analyzeSubSelectors(path)
if ok {
if len(path) == 0 || (path[0] == '|' || path[0] == '.') {
var b []byte
b = append(b, kind)
var i int
for _, sub := range subs {
res := Get(json, sub.path)
if res.Exists() {
if i > 0 {
b = append(b, ',')
}
if kind == '{' {
if len(sub.name) > 0 {
if sub.name[0] == '"' && IsValidJSON(sub.name) {
b = append(b, sub.name...)
} else {
b = appendJSON(b, sub.name)
}
} else {
last := lastSegment(sub.path)
if isValidName(last) {
b = appendJSON(b, last)
} else {
b = appendJSON(b, "_")
}
}
b = append(b, ':')
}
var raw string
if len(res.unprocessed) == 0 {
raw = res.String()
if len(raw) == 0 {
raw = "null"
}
} else {
raw = res.unprocessed
}
b = append(b, raw...)
i++
}
}
b = append(b, kind+2)
var res Context
res.unprocessed = string(b)
res.kind = JSON
if len(path) > 0 {
res = res.Get(path[1:])
}
res.index = 0
return res
}
}
}
}
var i int
var c = &parser{json: json}
if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
c.lines = true
analyzeArray(c, 0, path[2:])
} else {
for ; i < len(c.json); i++ {
if c.json[i] == '{' {
i++
parseJSONObject(c, i, path)
break
}
if c.json[i] == '[' {
i++
analyzeArray(c, i, path)
break
}
}
}
if c.piped {
res := c.value.Get(c.pipe)
res.index = 0
return res
}
computeIndex(json, c)
return c.value
}
// GetMul searches json for multiple paths.
// The return value is a slice of `Context` objects, where the number of items
// will be equal to the number of input paths. Each `Context` represents the value
// extracted for the corresponding path.
//
// Parameters:
// - `json`: A string containing the JSON data to search through.
// - `path`: A variadic list of paths to search for within the JSON data.
//
// Returns:
// - A slice of `Context` objects, one for each path provided in the `path` parameter.
//
// Notes:
// - The function will return a `Context` for each path, and the order of the `Context`
// objects in the result will match the order of the paths provided.
//
// Example:
//
// json := `{
// "user": {"firstName": "Alice", "lastName": "Johnson"},
// "age": 29,
// "siblings": ["Ben", "Clara", "David"],
// "friends": [
// {"firstName": "Tom", "lastName": "Smith"},
// {"firstName": "Sophia", "lastName": "Davis"}
// ]
// }`
// paths := []string{"user.lastName", "age", "siblings.#", "friends.#.firstName"}
// results := GetMul(json, paths...)
// // The result will contain Contexts for each path: ["Johnson", 29, 3, ["Tom", "Sophia"]]
func GetMul(json string, path ...string) []Context {
ctx := make([]Context, len(path))
for i, path := range path {
ctx[i] = Get(json, path)
}
return ctx
}
// GetBytes searches the provided JSON byte slice for the specified path and returns a `Context`
// representing the extracted data. This method is preferred over `Get(string(data), path)` when working
// with JSON data in byte slice format, as it directly operates on the byte slice, minimizing memory
// allocations and unnecessary copies.
//
// Parameters:
// - `json`: A byte slice containing the JSON data to process.
// - `path`: A string representing the path in the JSON data to extract.
//
// Returns:
// - A `Context` struct containing the processed JSON data. The `Context` struct includes both
// the raw unprocessed JSON string and the specific extracted string based on the given path.
//
// Notes:
// - This function internally calls the `getBytes` function, which uses unsafe pointer operations
// to minimize allocations and efficiently handle string slice headers.
// - The function avoids unnecessary memory allocations by directly processing the byte slice and
// utilizing memory safety features to manage substring extraction when the `strings` part is
// a substring of the `unprocessed` part of the JSON data.
//
// Example:
//
// jsonBytes := []byte(`{"key": "value", "nested": {"innerKey": "innerValue"}}`)
// path := "nested.innerKey"
// context := GetBytes(jsonBytes, path)
// fmt.Println("Unprocessed:", context.unprocessed) // Output: `{"key": "value", "nested": {"innerKey": "innerValue"}}`
// fmt.Println("Strings:", context.strings) // Output: `"innerValue"`
func GetBytes(json []byte, path string) Context {
return getBytes(json, path)
}
// GetMulBytes searches json for multiple paths in the provided JSON byte slice.
// The return value is a slice of `Context` objects, where the number of items
// will be equal to the number of input paths. Each `Context` represents the value
// extracted for the corresponding path. This method operates directly on the byte slice,
// which is preferred when working with JSON data in byte format to minimize memory allocations.
//
// Parameters:
// - `json`: A byte slice containing the JSON data to search through.
// - `path`: A variadic list of paths to search for within the JSON data.
//
// Returns:
// - A slice of `Context` objects, one for each path provided in the `path` parameter.
//
// Notes:
// - The function will return a `Context` for each path, and the order of the `Context`
// objects in the result will match the order of the paths provided.
//
// Example:
//
// jsonBytes := []byte(`{"user": {"firstName": "Alice", "lastName": "Johnson"}, "age": 29}`)
// paths := []string{"user.lastName", "age"}
// results := GetMulBytes(jsonBytes, paths...)
// // The result will contain Contexts for each path: ["Johnson", 29]
func GetMulBytes(json []byte, path ...string) []Context {
ctx := make([]Context, len(path))
for i, path := range path {
ctx[i] = GetBytes(json, path)
}
return ctx
}
// Foreach iterates through each line of JSON data in the JSON Lines format (http://jsonlines.org/),
// and applies a provided iterator function to each line. This is useful for processing large JSON data
// sets where each line is a separate JSON object, allowing for efficient parsing and handling of each object.
//
// Parameters:
// - `json`: A string containing JSON Lines formatted data, where each line is a separate JSON object.
// - `iterator`: A callback function that is called for each line. It receives a `Context` representing
// the parsed JSON object for the current line. The iterator function should return `true` to continue
// processing the next line, or `false` to stop the iteration.
//
// Example Usage:
//
// json := `{"name": "Alice"}\n{"name": "Bob"}`
// iterator := func(line Context) bool {
// fmt.Println(line)
// return true
// }
// Foreach(json, iterator)
// // Output:
// // {"name": "Alice"}
// // {"name": "Bob"}
//
// Notes:
// - This function assumes the input `json` is formatted as JSON Lines, where each line is a valid JSON object.
// - The function stops processing as soon as the `iterator` function returns `false` for a line.
// - The function handles each line independently, meaning it processes one JSON object at a time and provides
// it to the iterator, which can be used to process or filter lines.
//
// Returns:
// - This function does not return a value. It processes the JSON data line-by-line and applies the iterator to each.
func Foreach(json string, iterator func(line Context) bool) {
var ctx Context
var i int
for {
i, ctx, _ = parseJSONAny(json, i, true)
if !ctx.Exists() {
break
}
if !iterator(ctx) {
return
}
}
}
// IsValidJSON checks whether the provided string contains valid JSON data.
// It attempts to parse the JSON and returns a boolean indicating if the JSON is well-formed.
//
// Parameters:
// - `json`: A string representing the JSON data that needs to be validated.
//
// Returns:
// - A boolean value (`true` or `false`):
// - `true`: The provided JSON string is valid and well-formed.
// - `false`: The provided JSON string is invalid or malformed.
//
// Notes:
// - This function utilizes the `fromStr2Bytes` function to efficiently convert the input string
// into a byte slice without allocating new memory. It then passes the byte slice to the
// `verifyJSON` function to check if the string conforms to valid JSON syntax.
// - If the input JSON is invalid, the function will return `false`, indicating that the JSON
// cannot be parsed or is improperly structured.
// - The function does not perform deep validation of the content of the JSON, but merely
// checks if the string is syntactically correct according to JSON rules.
//
// Example Usage:
//
// json := `{"name": {"first": "Alice", "last": "Johnson"}, "age": 30}`
// if !IsValidJSON(json) {
// fmt.Println("Invalid JSON")
// } else {
// fmt.Println("IsValidJSON JSON")
// }
//
// // Output: "IsValidJSON JSON"
func IsValidJSON(json string) bool {
_, ok := verifyJSON(unsafeStringToBytes(json), 0)
return ok
}
// IsValidJSONBytes checks whether the provided byte slice contains valid JSON data.
// It attempts to parse the JSON and returns a boolean indicating if the JSON is well-formed.
//
// Parameters:
// - `json`: A byte slice (`[]byte`) representing the JSON data that needs to be validated.
//
// Returns:
// - A boolean value (`true` or `false`):
// - `true`: The provided JSON byte slice is valid and well-formed.
// - `false`: The provided JSON byte slice is invalid or malformed.
//
// Notes:
// - This function works directly with a byte slice (`[]byte`) rather than a string, making it more efficient
// when dealing with raw byte data that represents JSON. It avoids the need to convert between strings and
// byte slices, which can improve performance and memory usage when working with large or binary JSON data.
// - The function utilizes the `verifyJSON` function to check if the byte slice conforms to valid JSON syntax.
// - If the input byte slice represents invalid JSON, the function will return `false`, indicating that the JSON
// cannot be parsed or is improperly structured.
// - The function does not perform deep validation of the content of the JSON, but only checks whether the byte slice
// adheres to the syntax rules defined for JSON data structures.
//
// Example Usage:
//
// jsonBytes := []byte(`{"name": {"first": "Alice", "last": "Johnson"}, "age": 30}`)
// if !IsValidJSONBytes(jsonBytes) {
// fmt.Println("Invalid JSON")
// } else {
// fmt.Println("Valid JSON")
// }
//
// // Output: "Valid JSON"
func IsValidJSONBytes(json []byte) bool {
_, ok := verifyJSON(json, 0)
return ok
}
// AddTransformer binds a custom transformer function to the fj syntax.
//
// This function allows users to register custom transformer functions that can be applied
// to JSON data in the fj query language. A transformer is a transformation function that
// takes two string arguments — the JSON data and an argument (such as a key or value) —
// and returns a modified version of the JSON data. The registered transformer can then
// be used in queries to modify the JSON data dynamically.
//
// Parameters:
// - `name`: A string representing the name of the transformer. This name will be used
// in the fj query language to reference the transformer.
// - `fn`: A function that takes two string arguments: `json` (the JSON data to be modified),
// and `arg` (the argument that the transformer will use to transform the JSON). The function
// should return a modified string (the transformed JSON data).
//
// Example Usage:
//
// // Define a custom transformer to uppercase all values in a JSON array.
// uppercaseTransformer := func(json, arg string) string {
// return strings.ToUpper(json) // Modify the JSON data (this is just a simple example).
// }
//
// // Add the custom transformer to the fj system with the name "uppercase".
// fj.AddTransformer("uppercase", uppercaseTransformer)
//
// // Now you can use the "uppercase" transformer in a query:
// json := `{
// "store": {
// "book": [
// { "category": "fiction", "author": "J.K. Rowling", "title": "Harry Potter" },
// { "category": "science", "author": "Stephen Hawking", "title": "A Brief History of Time" }
// ],
// "music": [
// { "artist": "The Beatles", "album": "Abbey Road" },
// { "artist": "Pink Floyd", "album": "The Wall" }
// ]
// }
// }`
// result := fj.Get(json, "store.music.1.album|@uppercase").String() // Applies the uppercase transformer to each value in the array.
// // result will contain: THE WALL
//
// Notes:
// - This function is not thread-safe, so it should be called once, typically during
// the initialization phase, before performing any queries that rely on custom transformers.
// - Once registered, the transformer can be used in fj queries to transform the JSON data
// according to the logic defined in the `fn` function.
func AddTransformer(name string, fn func(json, arg string) string) {
jsonTransformers[name] = fn
}
// IsTransformerRegistered checks whether a specified transformer has been registered in the fj system.
//
// This function allows users to verify if a transformer with a given name has already
// been added to the `fj` query system. transformers are custom functions that transform
// JSON data in queries. This utility is useful to prevent duplicate registrations
// or to confirm the availability of a specific transformer before using it.
//
// Parameters:
// - `name`: A string representing the name of the transformer to check for existence.
//
// Returns:
// - `bool`: Returns `true` if a transformer with the given name exists, otherwise returns `false`.
//
// Example Usage:
//
// // Check if a custom transformer named "uppercase" has already been registered.
// if fj.IsTransformerRegistered("uppercase") {
// fmt.Println("The 'uppercase' transformer is available.")
// } else {
// fmt.Println("The 'uppercase' transformer has not been registered.")
// }
//
// Notes:
// - This function does not modify the `transformers` map; it only queries it to check
// for the existence of the specified transformer.
// - It is thread-safe when used only to query the existence of a transformer.
func IsTransformerRegistered(name string) bool {
if isEmpty(name) {
return false
}
if len(jsonTransformers) == 0 {
return false
}
_, ok := jsonTransformers[name]
return ok
}
// Kind returns the JSON type of the Context.
// It provides the specific type of the JSON value, such as String, Number, Object, etc.
//
// Returns:
// - Type: The type of the JSON value represented by the Context.
func (ctx Context) Kind() Type {
return ctx.kind
}
// Unprocessed returns the raw, unprocessed JSON string for the Context.
// This can be useful for inspecting the original data without any parsing or transformations.
//
// Returns:
// - string: The unprocessed JSON string.
func (ctx Context) Unprocessed() string {
return ctx.unprocessed
}
// Numeric returns the numeric value of the Context, if applicable.
// This is relevant when the Context represents a JSON number.
//
// Returns:
// - float64: The numeric value of the Context.
// If the Context does not represent a number, the value may be undefined.
func (ctx Context) Numeric() float64 {
return ctx.numeric
}
// Index returns the index of the unprocessed JSON value in the original JSON string.
// This can be used to track the position of the value in the source data.
// If the index is unknown, it defaults to 0.
//
// Returns:
// - int: The position of the value in the original JSON string.
func (ctx Context) Index() int {
return ctx.index
}
// Indexes returns a slice of indices for elements matching a path containing the '#' character.
// This is useful for handling path queries that involve multiple matches.
//
// Returns:
// - []int: A slice of indices for matching elements.
func (ctx Context) Indexes() []int {
return ctx.indexes
}
// String returns a string representation of the Context value.
// The output depends on the JSON type of the Context:
// - For `False` type: Returns "false".
// - For `True` type: Returns "true".
// - For `Number` type: Returns the numeric value as a string.
// If the numeric value was calculated, it formats the float value.
// Otherwise, it preserves the original unprocessed string if valid.
// - For `String` type: Returns the string value.
// - For `JSON` type: Returns the raw unprocessed JSON string.
// - For other types: Returns an empty string.
//
// Returns:
// - string: A string representation of the Context value.
func (ctx Context) String() string {
switch ctx.kind {
default:
return ""
case True:
return "true"
case False:
return "false"
case String:
return ctx.strings
case JSON:
return ctx.unprocessed
case Number:
if len(ctx.unprocessed) == 0 {
return strconv.FormatFloat(ctx.numeric, 'f', -1, 64)
}
var i int
if ctx.unprocessed[0] == '-' {
i++
}
for ; i < len(ctx.unprocessed); i++ {
if ctx.unprocessed[i] < '0' || ctx.unprocessed[i] > '9' {
return strconv.FormatFloat(ctx.numeric, 'f', -1, 64)
}
}
return ctx.unprocessed
}
}
// StringColored returns a colored string representation of the Context value.
// It applies the default style defined in `defaultStyle` to the string
// representation of the Context value.
//
// Details:
// - The function first retrieves the plain string representation using `ctx.String()`.
// - If the string is empty (determined by `isEmpty`), it returns an empty string.
// - Otherwise, it applies the coloring rules from `defaultStyle` using the
// `unify4g.Color` function.
//
// Returns:
// - string: A colored string representation of the Context value if not empty.
// Returns an empty string for empty or invalid Context values.
//
// Example Usage:
//
// ctx := Context{kind: True}
// fmt.Println(ctx.StringColored()) // Output: "\033[1;35mtrue\033[0m" (colored)
//
// Notes:
// - Requires the `unify4g` library for styling and the `isEmpty` utility function
// to check for empty strings.
func (ctx Context) StringColored() string {
s := []byte(ctx.String())
if isEmpty(string(s)) {
return ""
}
return string(unify4g.Color(s, defaultStyle))
}
// WithStringColored applies a customizable colored styling to the string representation of the Context value.
//
// This function enhances the default coloring functionality by allowing the caller to specify a custom
// style for highlighting the Context value. If no custom style is provided, the default styling rules
// (`defaultStyle`) are used.
//
// Parameters:
// - style (*unify4g.Style): A pointer to a Style structure that defines custom styling rules
// for JSON elements. If `style` is nil, the `defaultStyle` is applied.
//
// Details:
// - Retrieves the plain string representation of the Context value using `ctx.String()`.
// - Checks if the string is empty using the `isEmpty` utility function. If empty, it returns
// an empty string immediately.
// - If a custom style is provided, it applies the given style to the string representation
// using the `unify4g.Color` function. Otherwise, it applies the default style.
//
// Returns:
// - string: A styled string representation of the Context value based on the provided or default style.
//
// Example Usage:
//
// customStyle := &unify4g.Style{
// Key: [2]string{"\033[1;36m", "\033[0m"},
// String: [2]string{"\033[1;33m", "\033[0m"},
// // Additional styling rules...
// }
//
// ctx := Context{kind: True}
// fmt.Println(ctx.WithStringColored(customStyle)) // Output: "\033[1;35mtrue\033[0m" (custom colored)
//
// Notes:
// - The function uses the `unify4g.Color` utility to apply the color rules defined in the style.
// - Requires the `isEmpty` utility function to check for empty strings.
func (ctx Context) WithStringColored(style *unify4g.Style) string {
s := []byte(ctx.String())
if isEmpty(string(s)) {
return ""
}
if style == nil {
style = defaultStyle
}
return string(unify4g.Color(s, style))
}
// Bool converts the Context value into a boolean representation.
// The conversion depends on the JSON type of the Context:
// - For `True` type: Returns `true`.
// - For `String` type: Attempts to parse the string as a boolean (case-insensitive).
// If parsing fails, defaults to `false`.
// - For `Number` type: Returns `true` if the numeric value is non-zero, otherwise `false`.
// - For all other types: Returns `false`.
//
// Returns:
// - bool: A boolean representation of the Context value.
func (ctx Context) Bool() bool {
switch ctx.kind {
default:
return false
case True:
return true
case String:
b, _ := strconv.ParseBool(strings.ToLower(ctx.strings))
return b
case Number:
return ctx.numeric != 0
}
}
// Int64 converts the Context value into an integer representation (int64).
// The conversion depends on the JSON type of the Context:
// - For `True` type: Returns 1.
// - For `String` type: Attempts to parse the string into an integer. Defaults to 0 on failure.
// - For `Number` type:
// - Directly converts the numeric value to an integer if it's safe.
// - Parses the unprocessed string for integer values as a fallback.
// - Defaults to converting the float64 numeric value to an int64.
//
// Returns:
// - int64: An integer representation of the Context value.
func (ctx Context) Int64() int64 {
switch ctx.kind {
default:
return 0
case True:
return 1
case String:
n, _ := parseInt64(ctx.strings)
return n
case Number:
i, ok := ensureSafeInt64(ctx.numeric)
if ok {
return i
}
i, ok = parseInt64(ctx.unprocessed)
if ok {
return i
}
return int64(ctx.numeric)
}
}
// Uint64 converts the Context value into an unsigned integer representation (uint64).
// The conversion depends on the JSON type of the Context:
// - For `True` type: Returns 1.
// - For `String` type: Attempts to parse the string into an unsigned integer. Defaults to 0 on failure.
// - For `Number` type:
// - Directly converts the numeric value to a uint64 if it's safe and non-negative.
// - Parses the unprocessed string for unsigned integer values as a fallback.
// - Defaults to converting the float64 numeric value to a uint64.
//
// Returns:
// - uint64: An unsigned integer representation of the Context value.
func (ctx Context) Uint64() uint64 {
switch ctx.kind {
default:
return 0
case True:
return 1
case String:
n, _ := parseUint64(ctx.strings)
return n
case Number:
i, ok := ensureSafeInt64(ctx.numeric)
if ok && i >= 0 {
return uint64(i)
}
u, ok := parseUint64(ctx.unprocessed)
if ok {
return u
}
return uint64(ctx.numeric)
}
}
// Float64 converts the Context value into a floating-point representation (float64).
// The conversion depends on the JSON type of the Context:
// - For `True` type: Returns 1.
// - For `String` type: Attempts to parse the string as a floating-point number. Defaults to 0 on failure.
// - For `Number` type: Returns the numeric value as a float64.
//
// Returns:
// - float64: A floating-point representation of the Context value.
func (ctx Context) Float64() float64 {
switch ctx.kind {
default:
return 0
case True:
return 1
case String:
n, _ := strconv.ParseFloat(ctx.strings, 64)
return n
case Number:
return ctx.numeric
}
}
// Float32 converts the Context value into a floating-point representation (Float32).
// This function provides a similar conversion mechanism as Float64 but with Float32 precision.
// The conversion depends on the JSON type of the Context:
// - For `True` type: Returns 1 as a Float32 value.
// - For `String` type: Attempts to parse the string as a floating-point number (Float32 precision).
// If the parsing fails, it defaults to 0.
// - For `Number` type: Returns the numeric value as a Float32, assuming the Context contains
// a numeric value in its `numeric` field.
//
// Returns:
// - Float32: A floating-point representation of the Context value.
//
// Example Usage:
//
// ctx := Context{kind: String, strings: "123.45"}
// result := ctx.Float32()
// // result: 123.45 (parsed as Float32)
//
// ctx = Context{kind: True}
// result = ctx.Float32()
// // result: 1 (True is represented as 1.0)
//
// ctx = Context{kind: Number, numeric: 678.9}
// result = ctx.Float32()
// // result: 678.9 (as Float32)
//
// Details:
//
// - For the `True` type, the function always returns 1.0, representing the boolean `true` value.
//
// - For the `String` type, it uses `strconv.ParseFloat` with 32-bit precision to convert the string
// into a Float32. If parsing fails (e.g., if the string is not a valid numeric representation),
// the function returns 0 as a fallback.
//
// - For the `Number` type, the `numeric` field, assumed to hold a float64 value, is converted
// to a Float32 for the return value.
//
// Notes:
//
// - The function gracefully handles invalid string inputs for the `String` type by returning 0,
// ensuring no runtime panic occurs due to a parsing error.
//
// - Precision may be lost when converting from float64 (`numeric` field) to Float32.
func (ctx Context) Float32() float32 {