├── .gitignore
├── FMFiles
├── FileMaker-JSON-Functions.fmp12
└── Speed-Tests.fmp12
├── FunctionIdList.md
├── Functions
├── jsonAv.fmfn
├── jsonGet.fmfn
├── jsonGetKeyList.fmfn
├── jsonModify.fmfn
├── jsonOp.fmfn
├── z_jsonEncodeSupport.fmfn
├── z_jsonParseSupport1.fmfn
├── z_jsonParseSupport2.fmfn
└── z_jsonParseSupport3.fmfn
├── LICENSE.md
├── README.md
└── recursion_notes.md
/.gitignore:
--------------------------------------------------------------------------------
1 | Archive/
2 | commit.*
3 | *.lnk
4 | Import.log
5 | CFUpdater.fmp12
--------------------------------------------------------------------------------
/FMFiles/FileMaker-JSON-Functions.fmp12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/d25297ed403fcd9fd264d0982cae192c4a0c95ef/FMFiles/FileMaker-JSON-Functions.fmp12
--------------------------------------------------------------------------------
/FMFiles/Speed-Tests.fmp12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/d25297ed403fcd9fd264d0982cae192c4a0c95ef/FMFiles/Speed-Tests.fmp12
--------------------------------------------------------------------------------
/FunctionIdList.md:
--------------------------------------------------------------------------------
1 | List of functionId's sent to the z_jsonParseSupport functions to identify the section of code to run. I reference this file when viewing logs, which record the function Id
2 |
3 | - 100 ParseValue
4 | - 101 ParseValueSkip
5 | - 110 FindValue
6 | - 120 FindEnd
7 | - 121 IsEmpty
8 | - 199 Error
9 |
10 | - 200 ParseObjectFindEnd
11 | - 201 ParseObjectFindValue
12 | - 202 ParseObjectGetKeyList
13 | - 210 ParseArrayFindEnd
14 | - 211 ParseArrayFindValue
15 | - 212 ParseArrayGetKeyList
16 | - 220 GetKeyListFromCache
17 | - 221 GetIndexListFromCache
18 |
19 | - 300 ParseWhitespace
20 | - 310 ParseString
21 | - 311 ParseHex
22 | - 320 ParseNumber
23 | - 321 ParseDigits
24 | - 330 ParseWord
25 |
--------------------------------------------------------------------------------
/Functions/jsonAv.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * jsonAv ( value )
4 | *
5 | * PURPOSE:
6 | * Prepares a string for inclusion as an element in a JSON array.
7 | *
8 | * PARAMETERS:
9 | * value = any value to be encoded
10 | *
11 | * DEPENDENCIES:
12 | * Custom Functions:
13 | * z_jsonEncodeSupport
14 | *
15 | * LICENSE:
16 | * See the LICENSE.md file for license rights and limitations (MIT):
17 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
18 | *
19 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
20 | *
21 | * HISTORY:
22 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
23 | * =====================================
24 | */
25 |
26 | If (
27 | /* return the value as-is if it's a json array or object */
28 | /**
29 | * TODO: this would incorrectly detect json on a value like:
30 | * {this is a note in curly-brances}
31 | * TODO: would also fail if there is leading/trailing whitespace
32 | */
33 | Let ( [
34 | ~firstAndLast = Left ( value ; 1 ) & Right ( value ; 1 )
35 | ] ;
36 | ~firstAndLast = "[]" /* array */
37 | or ~firstAndLast = "{}" /* object */
38 | ) ;
39 | value ;
40 |
41 | z_jsonEncodeSupport ( 1 /* Value */ ; value )
42 | )
43 |
44 | & ","
--------------------------------------------------------------------------------
/Functions/jsonGet.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * jsonGet ( json ; keyOrIndex )
4 | *
5 | * RETURNS:
6 | * The value specified by keyOrIndex if it exists, otherwise return "json:notFound".
7 | * If json value is null, will return "json:null"
8 | * true/false will be returned as the FileMaker equivalent 1/0.
9 | *
10 | * PARAMETERS:
11 | * json = the json string
12 | * keyOrIndex = object property key (name) or array index (position)
13 | *
14 | * DEPENDENCIES:
15 | * Custom Functions:
16 | * z_jsonParseSupport1
17 | *
18 | * NOTES:
19 | *
20 | * LICENSE:
21 | * See the LICENSE.md file for license rights and limitations (MIT):
22 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
23 | *
24 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
25 | *
26 | * HISTORY:
27 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
28 | * =====================================
29 | */
30 |
31 | z_jsonParseSupport1 ( 130 /* jsonGet */ ; json ; keyOrIndex ; "" ; "" )
--------------------------------------------------------------------------------
/Functions/jsonGetKeyList.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * jsonGetKeyList ( json )
4 | *
5 | * RETURNS:
6 | * Return-delimited list of member properties from an object or indexes
7 | * from an array. It only goes one level deep.
8 | *
9 | * PARAMETERS:
10 | * json = the json string
11 | *
12 | * DEPENDENCIES:
13 | * Custom Functions:
14 | * z_jsonParseSupport1
15 | *
16 | * NOTES:
17 | *
18 | * LICENSE:
19 | * See the LICENSE.md file for license rights and limitations (MIT):
20 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
21 | *
22 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
23 | *
24 | * HISTORY:
25 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
26 | * =====================================
27 | */
28 |
29 | z_jsonParseSupport1 ( 132 /* jsonGetKeyList */ ; json ; "" ; "" ; "" )
30 |
--------------------------------------------------------------------------------
/Functions/jsonModify.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * jsonModify ( json ; keyOrIndex ; newValue )
4 | *
5 | * PURPOSE:
6 | * Add or modify an object by it's key, or array by it's index.
7 | *
8 | * RETURNS:
9 | * The modified JSON.
10 | *
11 | * PARAMETERS:
12 | * json = the json string
13 | * keyOrIndex = object property key (name) or array index (position)
14 | * newValue = string, number, or a named constant:
15 | * json:true, json:false, json:null
16 | * should NOT be an object or array (these will be encoded as strings)
17 | *
18 | * DEPENDENCIES:
19 | * Custom Functions:
20 | * z_jsonParseSupport1
21 | *
22 | * LICENSE:
23 | * See the LICENSE.md file for license rights and limitations (MIT):
24 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
25 | *
26 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
27 | *
28 | * HISTORY:
29 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
30 | * =====================================
31 | */
32 |
33 | z_jsonParseSupport1 ( 131 /* jsonModify */ ; json ; keyOrIndex ; newValue ; "" )
34 |
--------------------------------------------------------------------------------
/Functions/jsonOp.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * jsonOp ( key ; value )
4 | *
5 | * PURPOSE:
6 | * Encodes a key value pair as an JSON object
7 | *
8 | * PARAMETERS:
9 | * key = the name of the property
10 | * value = any value to be encoded
11 | *
12 | * DEPENDENCIES:
13 | * Custom Functions:
14 | * z_jsonEncodeSupport
15 | *
16 | * LICENSE:
17 | * See the LICENSE.md file for license rights and limitations (MIT):
18 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
19 | *
20 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
21 | *
22 | * HISTORY:
23 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
24 | * =====================================
25 | */
26 |
27 | z_jsonEncodeSupport ( 2 /* String */ ; key )
28 | & ":"
29 | & If (
30 | /* return the value as-is if it's a json array or object */
31 | /**
32 | * TODO: this would incorrectly detect json on a value like:
33 | * {this is a note in curly-brances}
34 | * TODO: would also fail if there is leading/trailing whitespace
35 | */
36 | Let ( [
37 | ~firstAndLast = Left ( value ; 1 ) & Right ( value ; 1 )
38 | ] ;
39 | ~firstAndLast = "[]" /* array */
40 | or ~firstAndLast = "{}" /* object */
41 | ) ;
42 | value ;
43 |
44 | z_jsonEncodeSupport ( 1 /* Value */ ; value )
45 | )
46 | & ","
--------------------------------------------------------------------------------
/Functions/z_jsonEncodeSupport.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * z_jsonEncodeSupport ( functionId ; req )
4 | *
5 | * PURPOSE:
6 | * Supporting code for native json encoding functions.
7 | *
8 | * PARAMETERS:
9 | * functionId = numeric code for function to run
10 | * 1 Value
11 | * 2 String
12 | * 3 Number
13 | * req = requested data (if any)
14 | *
15 | * DEPENDENCIES:
16 | * none
17 | *
18 | * LICENSE:
19 | * See the LICENSE.md file for license rights and limitations (MIT):
20 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
21 | *
22 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
23 | *
24 | * HISTORY:
25 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
26 | * =====================================
27 | */
28 |
29 | Case (
30 |
31 |
32 | functionId = 1 ;
33 | /**
34 | * =====================================
35 | * Value ( req )
36 | *
37 | * RETURNS:
38 | * JSON encoded value.
39 | *
40 | * PARAMETERS:
41 | * req = string, number, or a named constant:
42 | * json:true, json:false, json:null
43 | * should NOT be an object or array (these will be encoded as strings)
44 | *
45 | * NOTES:
46 | * named constants are NOT case sensitive
47 | * =====================================
48 | */
49 |
50 | Case (
51 | /* named constants */
52 | req = "json:true" ;
53 | "true" ;
54 | req = "json:false" ;
55 | "false" ;
56 | req = "json:null" ;
57 | "null" ;
58 |
59 | IsEmpty ( req ) ;
60 | "\"\"" ;
61 |
62 | req = "?" ;
63 | "\"?\"" ;
64 |
65 | /**
66 | * req is too long to be a number
67 | *
68 | * Technically speaking, a number could be longer, but it rarely is. Hard-coding
69 | * this threshold improves performance slightly.
70 | * This threshold can be increased (or deleted) to support longer numbers, if needed.
71 | */
72 | Length ( req ) > 1000
73 | or req ≠ GetAsNumber ( req )
74 | ;
75 | z_jsonEncodeSupport ( 2 /* String */ ; req ) ;
76 |
77 | /* else: assume it's a number */
78 | z_jsonEncodeSupport ( 3 /* Number */ ; req )
79 | ) ;
80 |
81 |
82 | functionId = 2 ;
83 | /**
84 | * =====================================
85 | * String ( req )
86 | *
87 | * RETURNS:
88 | * JSON encoded string.
89 | *
90 | * PARAMETERS:
91 | * req = any value to be escaped as a string
92 | * =====================================
93 | */
94 |
95 | Let ( [
96 | ~string = Substitute (
97 | req ;
98 | [ "\\" ; "\\\\" ] ; // reverse solidus
99 | [ "\"" ; "\\\"" ] ; // quotation mark
100 | [ Char ( 8 ) ; "\b" ] ; // backspace
101 | [ Char ( 12 ) ; "\f" ] ; // formfeed
102 | [ Char ( 10 ) ; "\n" ] ; // newline
103 | [ Char ( 13 ) ; "\r" ] ; // carriage return
104 | [ Char ( 9 ) ; "\t" ] // horizontal tab
105 | ) ;
106 | ~controlCharacters = Filter (
107 | ~string ;
108 | Char ( 1 ) & Char ( 2 ) & Char ( 3 ) & Char ( 4 ) & Char ( 5 ) & Char ( 6 ) & Char ( 7 ) & Char ( 11 ) & Char ( 14 ) & Char ( 15 ) & Char ( 16 ) & Char ( 17 ) & Char ( 18 ) & Char ( 19 ) & Char ( 20 ) & Char ( 21 ) & Char ( 22 ) & Char ( 23 ) & Char ( 24 ) & Char ( 25 ) & Char ( 26 ) & Char ( 27 ) & Char ( 28 ) & Char ( 29 ) & Char ( 30 ) & Char ( 31 )
109 | ) ;
110 | ~string = If ( not IsEmpty ( ~controlCharacters ) ;
111 | Substitute (
112 | ~string ;
113 | [ Char ( 1 ) ; "\u0001" ] ;
114 | [ Char ( 2 ) ; "\u0002" ] ;
115 | [ Char ( 3 ) ; "\u0003" ] ;
116 | [ Char ( 4 ) ; "\u0004" ] ;
117 | [ Char ( 5 ) ; "\u0005" ] ;
118 | [ Char ( 6 ) ; "\u0006" ] ;
119 | [ Char ( 7 ) ; "\u0007" ] ;
120 | [ Char ( 11 ) ; "\u000B" ] ;
121 | [ Char ( 14 ) ; "\u000E" ] ;
122 | [ Char ( 15 ) ; "\u000F" ] ;
123 | [ Char ( 16 ) ; "\u0010" ] ;
124 | [ Char ( 17 ) ; "\u0011" ] ;
125 | [ Char ( 18 ) ; "\u0012" ] ;
126 | [ Char ( 19 ) ; "\u0013" ] ;
127 | [ Char ( 20 ) ; "\u0014" ] ;
128 | [ Char ( 21 ) ; "\u0015" ] ;
129 | [ Char ( 22 ) ; "\u0016" ] ;
130 | [ Char ( 23 ) ; "\u0017" ] ;
131 | [ Char ( 24 ) ; "\u0018" ] ;
132 | [ Char ( 25 ) ; "\u0019" ] ;
133 | [ Char ( 26 ) ; "\u001A" ] ;
134 | [ Char ( 27 ) ; "\u001B" ] ;
135 | [ Char ( 28 ) ; "\u001C" ] ;
136 | [ Char ( 29 ) ; "\u001D" ] ;
137 | [ Char ( 30 ) ; "\u001E" ] ;
138 | [ Char ( 31 ) ; "\u001F" ]
139 | ) ;
140 | ~string
141 | )
142 | ] ;
143 | If ( not IsEmpty ( ~string ) ;
144 | "\"" & ~string & "\"" ;
145 | "null"
146 | )
147 | ) ;
148 |
149 |
150 | functionId = 3 ;
151 | /**
152 | * =====================================
153 | * Number ( req )
154 | *
155 | * RETURNS:
156 | * JSON encoded number.
157 | *
158 | * PARAMETERS:
159 | * req = a valid number
160 | * =====================================
161 | */
162 |
163 | Case (
164 | Left ( req ; 2 ) = "-." ;
165 | Substitute ( req ; "-." ; "-0." ) ;
166 |
167 | Left ( req ; 1 ) = "." ;
168 | "0" & req ;
169 |
170 | req
171 | ) ;
172 |
173 |
174 |
175 | /**
176 | * =====================================================================
177 | * ELSE: FUNCTION NOT FOUND
178 | * =====================================================================
179 | */
180 | Let ( [
181 | $json.error = "FunctionId [" & functionId & "]does not exist"
182 | ] ;
183 | "?"
184 | )
185 | )
--------------------------------------------------------------------------------
/Functions/z_jsonParseSupport1.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * z_jsonParseSupport1 ( functionId ; req ; private ; res ; step )
4 | *
5 | * PURPOSE:
6 | * Supporting code for native json parsing functions.
7 | * high-level/supporting/control/dispatch
8 | *
9 | * PARAMETERS:
10 | * functionId = numeric code for function to run, in range of 100 - 199
11 | * 100 ParseValue
12 | * 101 ParseValueSkip
13 | * 110 GetValuePosition
14 | * 120 GetEndPosition
15 | * 121 IsEmpty
16 | * 130 jsonGet
17 | * 131 jsonModify
18 | * 132 jsonGetKeyList
19 | * 140 initializeCache
20 | * 199 Error
21 | * req = requested data (if any)
22 | * private = private data, likely sent to recursive calls of a function
23 | * res = response
24 | * step = current state of a recursive function
25 | *
26 | * DEPENDENCIES:
27 | * Custom Functions:
28 | * z_jsonParseSupport1 - 3
29 | * z_jsonEncodeSupport
30 | *
31 | * LICENSE:
32 | * See the LICENSE.md file for license rights and limitations (MIT):
33 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
34 | *
35 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
36 | *
37 | * HISTORY:
38 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
39 | * =====================================
40 | */
41 |
42 | /* logging start disabled
43 | Let ( [
44 | uuid = Get ( UUID ) ;
45 | ~! = LogWriterMemoryCreateEntry (
46 | "z_jsonParseSupport1"
47 | & " [functionId:" & functionId & "]"
48 | & " [uuid:" & uuid & "]"
49 | & " [req:" & req & "]"
50 | & " [private:" & private & "]"
51 | & " [res:" & res & "]"
52 | & " [step:" & step & "]"
53 | )
54 | ] ;
55 | disabled logging end */
56 | Case (
57 |
58 |
59 | functionId = 100 /* ParseValue */
60 | or functionId = 101 /* ParseValueSkip */
61 | ;
62 | /**
63 | * =====================================================================
64 | * ParseValue / ParseValueSkip
65 | *
66 | * ParseValue: Extract the value at the current pointer's position.
67 | * ParseValueSkip: Find the end of a value.
68 | *
69 | * parameters:
70 | * req = not used
71 | * private = not used
72 | *
73 | * returns:
74 | * ParseValue: The extracted value. If the value is an object or array, return the raw json.
75 | * ParseValueSkip: empty string
76 | * =====================================================================
77 | */
78 | Let ( [
79 | ~! = If ( $~json.ch ≤ " " ;
80 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
81 | ) ;
82 | ~valueType = Case (
83 | $~json.ch = "{" ; "o" ;
84 | $~json.ch = "[" ; "a" ;
85 | $~json.ch = "\"" ; "s" ;
86 | $~json.ch = "-" or ( $~json.ch ≥ 0 and $~json.ch ≤ 9 ) ; "n" ;
87 | /* else: assume word */
88 | "w"
89 | ) ;
90 | res =
91 | Case (
92 | ~valueType = "o" or ~valueType = "a" ;
93 | Let ( [
94 | ~start = $~json.at - 1 ;
95 | ~! = If ( ~valueType = "o" ;
96 | z_jsonParseSupport2 ( 200 /* ParseObjectFindEnd */ ; "" ; "" ; "" ; "" ) ;
97 | z_jsonParseSupport2 ( 210 /* ParseArrayFindEnd */ ; "" ; "" ; "" ; "" )
98 | ) ;
99 | ~end = $~json.at
100 | ] ;
101 | If ( functionId = 100 /* ParseValue */
102 | and IsEmpty ( $json.error )
103 | ;
104 | Middle (
105 | $~json.text ;
106 | ~start ;
107 | ~end - ~start - 1
108 | )
109 | )
110 | ) ;
111 |
112 | ~valueType = "s" ;
113 | z_jsonParseSupport3 ( 310 /* ParseString */ ; "" ; "" ; "" ; "" ) ;
114 |
115 | ~valueType = "n" ;
116 | z_jsonParseSupport3 ( 320 /* ParseNumber */ ; "" ; "" ; "" ; "" ) ;
117 |
118 | /* else */
119 | z_jsonParseSupport3 ( 330 /* ParseWord */ ; "" ; "" ; "" ; "" )
120 | )
121 | ] ;
122 | Case ( not IsEmpty ( $json.error ) ;
123 | "?" ;
124 |
125 | functionId = 100 /* ParseValue */ ;
126 | res ;
127 |
128 | ""
129 | )
130 | ) ;
131 |
132 |
133 |
134 | functionId = 110 ;
135 | /**
136 | * =====================================================================
137 | * GetValuePosition
138 | *
139 | * Find the starting position of a value.
140 | *
141 | * parameters:
142 | * req = json
143 | * private = keyOrIndex
144 | *
145 | * returns:
146 | * ParseValue: The extracted value. If the value is an object or array, return the raw json.
147 | * ParseValueSkip: empty string
148 | * =====================================================================
149 | */
150 | Let ( [
151 | /* initialize parsing variables */
152 | $~json.text = req ;
153 | $~json.ch = Left ( $~json.text ; 1 ) ;
154 | $~json.at = 2 ;
155 | $json.error = "" ;
156 |
157 | ~! = z_jsonParseSupport1 ( 140 /* initializeCache */ ; "" ; "" ; "" ; "" ) ;
158 |
159 | ~! = If ( $~json.ch ≤ " " ;
160 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
161 | )
162 | ] ;
163 | Case (
164 | /* object */
165 | $~json.ch = "{" ;
166 | z_jsonParseSupport2 ( 201 /* ParseObjectFindValue */ ; private ; "" ; "" ; "" ) ;
167 |
168 | /* array */
169 | $~json.ch = "[" ;
170 | z_jsonParseSupport2 ( 211 /* ParseArrayFindValue */ ; private ; "" ; "" ; "" ) ;
171 |
172 | /* else: string, number, or word */
173 | False
174 | & z_jsonParseSupport1 ( 199 /* Error */ ;
175 | "value sent to GetValuePosition function was not an object or array" ; "" ; "" ; ""
176 | )
177 | )
178 |
179 | /* clean-up parsing variables */
180 | & Let ( [
181 | $~json.text = "" ;
182 | $~json.ch = "" ;
183 | $~json.at = "" ;
184 | $~json.depth = ""
185 | /* logging start disabled
186 | ; ~! = LogWriterMemoryCreateEntry (
187 | "GetValuePosition: end"
188 | & " [functionId:" & functionId & "]"
189 | & " [uuid:" & uuid & "]"
190 | & " [req:" & req & "]"
191 | & " [private:" & private & "]"
192 | & " [res:" & res & "]"
193 | & " [step:" & step & "]"
194 | & "¶[$~json.cache[1]:" & $~json.cache[1] & "]"
195 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
196 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
197 | ) disabled logging end */
198 | ] ;
199 | ""
200 | )
201 | ) ;
202 |
203 |
204 |
205 | functionId = 120 ;
206 | /**
207 | * =====================================================================
208 | * GetEndPosition
209 | *
210 | * returns:
211 | * Position of last non-whitespace character in the json.
212 | *
213 | * parameters:
214 | * req = json
215 | * private = current character
216 | * res = current position
217 | *
218 | * notes:
219 | * Accesses the json from a reserved variable, but could potentially
220 | * be modified to access it from the req parameter.
221 | * =====================================================================
222 | */
223 | Let ( [
224 | /* initialize parsing variables on first iteration */
225 | ~ch = If ( not step ;
226 | " " ;
227 | private
228 | ) ;
229 | ~at = If ( not step ;
230 | Length ( req ) ;
231 | res
232 | ) ;
233 |
234 | ~ch = Middle ( req ; ~at ; 1 ) ;
235 | ~at = ~at - 1 ;
236 | ~endFound =
237 | ~ch = "}"
238 | or ~ch = "]"
239 | ] ;
240 | If ( ~endFound ;
241 | ~at + 1 ;
242 | z_jsonParseSupport1 ( 120 /* GetEndPosition */ ; req ; ~ch ; ~at ; 1 )
243 | )
244 | ) ;
245 |
246 |
247 |
248 | functionId = 121 ;
249 | /**
250 | * =====================================================================
251 | * IsEmpty
252 | *
253 | * Determine if the json is empty: {} or [] (potentially with whitespace)
254 | *
255 | * parameters:
256 | * req = json
257 | * private = not used
258 | *
259 | * returns:
260 | * Boolean True if json is an empty object or array, False if it isn't.
261 | * =====================================================================
262 | */
263 | Let ( [
264 | /* initialize parsing variables */
265 | $~json.text = req ;
266 | $~json.ch = Left ( $~json.text ; 1 ) ;
267 | $~json.at = 2 ;
268 |
269 | ~! = If ( $~json.ch ≤ " " ;
270 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
271 | ) ;
272 |
273 | ~! = If ( $~json.ch ≠ "{" and $~json.ch ≠ "[" ;
274 | z_jsonParseSupport1 ( 199 /* Error */ ;
275 | "json was not an object or array as expected. Occurred in IsEmpty function." ; "" ; "" ; ""
276 | )
277 | ) ;
278 |
279 | /* function:next_no_result */
280 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
281 | $~json.at = $~json.at + 1 ;
282 |
283 | ~! = If ( $~json.ch ≤ " " ;
284 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
285 | ) ;
286 |
287 | ~ch = $~json.ch ;
288 |
289 | /* clean-up parsing variables */
290 | $~json.text = "" ;
291 | $~json.ch = "" ;
292 | $~json.at = "" ;
293 | $~json.depth = ""
294 | ] ;
295 | If ( IsEmpty ( $json.error ) ;
296 | ~ch = "}" or ~ch = "]"
297 | )
298 | ) ;
299 |
300 |
301 |
302 | functionId = 130 ; /* jsonGet */
303 | Let ( [
304 | json = req ;
305 | keyOrIndex = private ;
306 |
307 | ~cache = z_jsonParseSupport1 ( 110 /* GetValuePosition */ ; json ; keyOrIndex ; "" ; "" )
308 | ] ;
309 | Case (
310 | not IsEmpty ( $json.error ) ;
311 | "?" ;
312 |
313 | /**
314 | * value was found
315 | */
316 | ~cache ;
317 | Let ( [
318 | ~cacheAsList = Substitute ( ~cache ; "|" ; ¶ ) ;
319 | ~valStartPos = GetValue ( ~cacheAsList ; 4 ) ;
320 | ~valEndPos = GetValue ( ~cacheAsList ; 5 ) ;
321 | $~json.text = Middle ( json ; ~valStartPos ; ~valEndPos - ~valStartPos + 1 ) ;
322 | $~json.ch = Left ( $~json.text ; 1 ) ;
323 | $~json.at = 2 ;
324 | ~result = Case (
325 | $~json.ch = "{" or $~json.ch = "[" ;
326 | $~json.text ;
327 |
328 | $~json.ch = "\"" ;
329 | z_jsonParseSupport3 ( 310 /* ParseString */ ; "" ; "" ; "" ; "" ) ;
330 |
331 | $~json.ch = "-"
332 | or (
333 | $~json.ch ≥ 0
334 | and $~json.ch ≤ 9
335 | )
336 | ;
337 | GetAsNumber ( $~json.text ) ;
338 |
339 | /* else */
340 | z_jsonParseSupport3 ( 330 /* ParseWord */ ; "" ; "" ; "" ; "" )
341 | ) ;
342 |
343 | /* clean-up parsing variables */
344 | $~json.text = "" ;
345 | $~json.ch = "" ;
346 | $~json.at = "" ;
347 | $~json.depth = ""
348 | ] ;
349 | ~result
350 | ) ;
351 |
352 | /**
353 | * else: value was NOT found
354 | */
355 | ""
356 | )
357 | ) ;
358 |
359 |
360 |
361 | functionId = 131 ; /* jsonModify */
362 | Let ( [
363 | json = req ;
364 | keyOrIndex = private ;
365 | newValue = res ;
366 |
367 | ~cache = z_jsonParseSupport1 ( 110 /* GetValuePosition */ ; json ; keyOrIndex ; "" ; "" ) ;
368 | ~newValue = If ( IsEmpty ( $json.error ) ;
369 | If (
370 | /* return the value as-is if it's a json array or object */
371 | /**
372 | * TODO: this would incorrectly detect json on a value like:
373 | * {this is a note in curly-brances}
374 | * TODO: would also fail if there is leading/trailing whitespace
375 | */
376 | Let ( [
377 | ~firstAndLast = Left ( newValue ; 1 ) & Right ( newValue ; 1 )
378 | ] ;
379 | ~firstAndLast = "[]" /* array */
380 | or ~firstAndLast = "{}" /* object */
381 | ) ;
382 | newValue ;
383 |
384 | z_jsonEncodeSupport ( 1 /* Value */ ; newValue )
385 | )
386 | )
387 | ] ;
388 | Case (
389 | not IsEmpty ( $json.error ) ;
390 | "?" ;
391 |
392 | /**
393 | * value was found
394 | */
395 | ~cache ;
396 | Let ( [
397 | ~cacheAsList = Substitute ( ~cache ; "|" ; ¶ ) ;
398 | ~valStartPos = GetValue ( ~cacheAsList ; 4 ) ;
399 | ~valEndPos = GetValue ( ~cacheAsList ; 5 ) ;
400 | ~jsonBefore = Middle (
401 | json ;
402 | 1 ;
403 | ~valStartPos - 1
404 | ) ;
405 | ~jsonAfter = Middle (
406 | json ;
407 | ~valEndPos + 1 ;
408 | Length ( json ) - ~valEndPos + 1
409 | )
410 | ] ;
411 | ~jsonBefore
412 | & ~newValue
413 | & ~jsonAfter
414 | ) ;
415 |
416 |
417 | /**
418 | * else: value was NOT found
419 | */
420 | Let ( [
421 | /**
422 | * determine if the json is empty: {} or []
423 | */
424 | ~isEmpty = If ( IsEmpty ( $json.error ) ;
425 | z_jsonParseSupport1 ( 121 /* IsEmpty */ ; json ; "" ; "" ; "" )
426 | ) ;
427 |
428 | /**
429 | * get the position of the last closing bracket
430 | */
431 | ~endPosition = If ( IsEmpty ( $json.error ) ;
432 | z_jsonParseSupport1 ( 120 /* GetEndPosition */ ; json ; "" ; "" ; "" )
433 | ) ;
434 |
435 | /**
436 | * get text before the closing bracket
437 | */
438 | ~jsonBefore = If ( IsEmpty ( $json.error ) ;
439 | Middle (
440 | json ;
441 | 1 ;
442 | ~endPosition - 1
443 | )
444 | ) ;
445 |
446 | /**
447 | * get closing bracket and all text after it
448 | * this will preserve original trailing whitespace
449 | */
450 | ~jsonAfter = If ( IsEmpty ( $json.error ) ;
451 | Middle (
452 | json ;
453 | ~endPosition ;
454 | Length ( json ) - ~endPosition + 1
455 | )
456 | ) ;
457 |
458 | /**
459 | * get closing bracket to determine if it's an object or array
460 | */
461 | ~closingBracket = If ( IsEmpty ( $json.error ) ;
462 | Middle ( json ; ~endPosition ; 1 )
463 | )
464 | ] ;
465 | If ( not IsEmpty ( $json.error ) ;
466 | "?" ;
467 |
468 | ~jsonBefore
469 | & If ( not ~isEmpty ; "," )
470 | & If ( ~closingBracket = "}" /* object */ ;
471 | z_jsonEncodeSupport ( 2 /* String */ ; keyOrIndex )
472 | & ":"
473 | )
474 | & ~newValue
475 | & ~jsonAfter
476 | )
477 | )
478 | )
479 | ) ;
480 |
481 |
482 | functionId = 132 ; /* jsonGetKeyList */
483 | Let ( [
484 | json = req ;
485 |
486 | /* initialize parsing variables */
487 | $~json.text = json ;
488 | $~json.ch = Middle ( $~json.text ; 1 ; 1 ) ;
489 | $~json.at = 2 ;
490 | $json.error = "" ;
491 |
492 | ~! = z_jsonParseSupport1 ( 140 /* initializeCache */ ; "" ; "" ; "" ; "" ) ;
493 |
494 | ~! = If ( $~json.ch ≤ " " ;
495 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
496 | )
497 | ] ;
498 | Case (
499 | /* object */
500 | $~json.ch = "{" ;
501 | z_jsonParseSupport2 ( 202 /* ParseObjectGetKeyList */ ; "" ; "" ; "" ; "" ) ;
502 |
503 | /* array */
504 | $~json.ch = "[" ;
505 | z_jsonParseSupport2 ( 212 /* ParseArrayGetKeyList */ ; "" ; "" ; "" ; "" ) ;
506 |
507 | /* else: string, number, or word */
508 | z_jsonParseSupport1 ( 199 /* Error */ ;
509 | "value sent to jsonGetKeyList function was not an object or array" ; "" ; "" ; ""
510 | )
511 | & "?"
512 | )
513 |
514 | & Let ( [
515 | /* clean-up parsing variables */
516 | $~json.text = "" ;
517 | $~json.ch = "" ;
518 | $~json.at = "" ;
519 | $~json.depth = ""
520 | /* logging start disabled
521 | ; ~! = LogWriterMemoryCreateEntry (
522 | "jsonGetKeyList: end"
523 | & " [functionId:" & functionId & "]"
524 | & " [uuid:" & uuid & "]"
525 | & " [req:" & req & "]"
526 | & " [private:" & private & "]"
527 | & " [res:" & res & "]"
528 | & " [step:" & step & "]"
529 | & "¶[$~json.cache[1]:" & $~json.cache[1] & "]"
530 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
531 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
532 | ) disabled logging end */
533 | ] ;
534 | ""
535 | )
536 | ) ;
537 |
538 |
539 |
540 | functionId = 140 ; /* initializeCache */
541 | Let ( [
542 | /**
543 | * =====================================================================
544 | * initialize cache
545 | *
546 | * $json.config.cache.enabled If true, cache will not be used.
547 | * Will automatically be set to true on FileMaker 12.
548 | *
549 | * $json.config.cache.maxdepth Maximum number of nested objects/arrays to cache.
550 | *
551 | * $~json.depth how many objects/arrays are above the current set of values
552 | * this is incremented for every new sub-object/array
553 | *
554 | * $~json.cache.hash ordered list of MD5 hashes of json objects/arrays
555 | *
556 | * $~json.cache.data ordered list of Quote()'ed cache data that corresponds with
557 | * the hash in the same position in it's list
558 | *
559 | * $~json.hash hash of the json object that temporary cache is associated with
560 | *
561 | * $~json.cache[n] temporary cache data for nth depth of the json in $~json.text,
562 | * which is currently being processed. Once an entire object/array
563 | * is cached, it is quoted and stored in $~json.cache.data.
564 | *
565 | * keyOrIndexName|keyStartPos|valueType|valueStartPos|valueEndPos|hash|lastAt¶
566 | *
567 | * keyOrIndexName with CR, LF and | removed
568 | * keyStartPos position of the first character of an object
569 | * property name. empty if an array
570 | * valueType json data type abbreviation: a,o,s,n,w
571 | * I'm still not sure if I need this.
572 | * valueStartPos position of the first character of the value
573 | * valueEndPos position of the last character of the value
574 | * hash if an object or array, the MD5 hash of it
575 | * used to find the cache for this object/array.
576 | * This value is not currently used, but would be
577 | * needed to implement jPath efficiently.
578 | * lastAt position of last parsed character
579 | *
580 | * all position values are an offset from the starting position of the
581 | * current object/array
582 | *
583 | * last value has these nuances:
584 | * - if the entire object/array is cached, will start with:
585 | * "|DONE|"
586 | * NOTE: I may want to add more information to this line,
587 | * but I'm not sure what yet.
588 | * NOTE: starting with | prevents collision with an object name
589 | * - else: will contain data on the last parsed value
590 | * - if the value was not completely parsed, then the last
591 | * value will not be complete, it will end with valueStartPos
592 | *
593 | * =====================================================================
594 | */
595 | $json.config.cache.enabled = If ( $json.config.cache.enabled = False ;
596 | False ;
597 | /* only enable cache if on version 13+ because it creates MD5 hashes via GetContainerAttribute function */
598 | GetAsNumber ( Get ( ApplicationVersion ) ) ≥ 13
599 | ) ;
600 | ~! = If ( $json.config.cache.enabled ;
601 | Let ( [
602 | $json.config.cache.maxdepth = If ( $json.config.cache.maxdepth ≥ 1 ;
603 | $json.config.cache.maxdepth ;
604 | 10 /* hard-coded default max depth */
605 | ) ;
606 | ~hash = GetContainerAttribute ( $~json.text ; "MD5" ) ;
607 | ~isNewJson = ~hash ≠ $~json.hash ; /* current json is different than last json accessed while caching was enabled */
608 | /* check if hash exists for previously accessed json */
609 | ~hashPosition = If ( ~isNewJson ;
610 | Position ( $~json.cache.hash ; $~json.hash ; 1 ; 1 )
611 | ) ;
612 |
613 | /* logging start disabled
614 | ~! = LogWriterMemoryCreateEntry (
615 | "GetValuePosition: initialize cache: start"
616 | & " [functionId:" & functionId & "]"
617 | & " [uuid:" & uuid & "]"
618 | & " [req:" & req & "]"
619 | & " [private:" & private & "]"
620 | & " [res:" & res & "]"
621 | & " [step:" & step & "]"
622 | & " [~hash:" & ~hash & "]"
623 | & " [~isNewJson:" & ~isNewJson & "]"
624 | & " [~hashPosition:" & ~hashPosition & "]"
625 | & "¶[$~json.cache[1]:" & $~json.cache[1] & "]"
626 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
627 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
628 | ) ; disabled logging end */
629 |
630 | /* remove previous cache for the previously accessed json, so the temporary cache can be saved */
631 | ~! = If ( ~hashPosition ;
632 | Let ( [
633 | ~leftOfHash = Left ( $~json.cache.hash ; ~hashPosition - 1 ) ;
634 | $~json.cache.hash = List (
635 | Left ( ~leftOfHash ; Length ( ~leftOfHash ) - 1 ) ;
636 | Right (
637 | $~json.cache.hash ;
638 | Length ( $~json.cache.hash ) - Length ( $~json.hash ) - ~hashPosition
639 | )
640 | ) ;
641 |
642 | ~valueNumber = PatternCount ( ~leftOfHash ; ¶ ) + 1 ;
643 | ~startOfData = Position ( $~json.cache.data ; ¶ ; 1 ; ~valueNumber - 1 ) + 1 ;
644 | ~endOfData = Position ( $~json.cache.data & ¶ ; ¶ ; ~startOfData ; 1 ) ;
645 | $~json.cache.data = List (
646 | Left ( $~json.cache.data ; ~startOfData - 2 ) ;
647 | Right (
648 | $~json.cache.data ;
649 | Length ( $~json.cache.data ) - ~endOfData
650 | )
651 | )
652 |
653 | /* logging start disabled
654 | ; ~! = LogWriterMemoryCreateEntry (
655 | "GetValuePosition: removed previous cache for previous json"
656 | & " [functionId:" & functionId & "]"
657 | & " [uuid:" & uuid & "]"
658 | & " [req:" & req & "]"
659 | & " [private:" & private & "]"
660 | & " [res:" & res & "]"
661 | & " [step:" & step & "]"
662 | & " [$~json.hash:" & $~json.hash & "]"
663 | & " [~hashPosition:" & ~hashPosition & "]"
664 | & " [~leftOfHash:" & ~leftOfHash & "]"
665 | & " [~valueNumber:" & ~valueNumber & "]"
666 | & " [~startOfData:" & ~startOfData & "]"
667 | & " [~endOfData:" & ~endOfData & "]"
668 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
669 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
670 | ) disabled logging end */
671 | ] ; "" )
672 | ) ;
673 |
674 | /* save the temporary cache to main storage */
675 | ~! = If ( ~isNewJson and not IsEmpty ( $~json.cache[1] ) ;
676 | Let ( [
677 | $~json.cache.hash = List ( $~json.cache.hash ; $~json.hash ) ;
678 | $~json.cache.data = List ( $~json.cache.data ; Quote ( $~json.cache[1] ) )
679 | /* logging start disabled
680 | ; ~! = LogWriterMemoryCreateEntry (
681 | "GetValuePosition: saved temporary cache from previous json"
682 | & " [functionId:" & functionId & "]"
683 | & " [uuid:" & uuid & "]"
684 | & " [req:" & req & "]"
685 | & " [private:" & private & "]"
686 | & " [res:" & res & "]"
687 | & " [step:" & step & "]"
688 | & " [$~json.hash:" & $~json.hash & "]"
689 | & " [~hashPosition:" & ~hashPosition & "]"
690 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
691 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
692 | ) disabled logging end */
693 | ] ; "" )
694 | ) ;
695 |
696 | /* clear temporary cache */
697 | ~! = If ( ~isNewJson ;
698 | Let ( [
699 | /* delete all $~json.cache[n] variables, up to $json.config.cache.maxdepth. */
700 | /* TODO: This method is temporary: do it properly
701 | possibly use a method like this to generate code as text, then evaluate it: http://www.briandunning.com/cf/942. I like this idea because I don't want to recurse to complete this task.
702 | */
703 | $~json.cache[1] = "" ;
704 | $~json.cache[2] = "" ;
705 | $~json.cache[3] = "" ;
706 | $~json.cache[4] = "" ;
707 | $~json.cache[5] = "" ;
708 | $~json.cache[6] = "" ;
709 | $~json.cache[7] = "" ;
710 | $~json.cache[8] = "" ;
711 | $~json.cache[9] = "" ;
712 | $~json.cache[10] = ""
713 | ] ; "" )
714 | ) ;
715 |
716 | /* now that temporary cache has been saved, modify $~json.hash to hold the value for the current json */
717 | $~json.hash = ~hash ;
718 |
719 | /* does cache exists for current json? */
720 | ~hashPosition = If ( ~isNewJson ;
721 | Position ( $~json.cache.hash ; $~json.hash ; 1 ; 1 )
722 | ) ;
723 |
724 | /* Load saved cache into temporary cache. */
725 | $~json.cache[1] = If ( ~hashPosition ;
726 | Let ( [
727 | ~leftOfHash = Left ( $~json.cache.hash ; ~hashPosition - 1 ) ;
728 | ~valueNumber = PatternCount ( ~leftOfHash ; ¶ ) + 1 ;
729 | ~data = GetValue ( $~json.cache.data ; ~valueNumber )
730 | /* logging start disabled
731 | ; ~! = LogWriterMemoryCreateEntry (
732 | "GetValuePosition: load saved cache to temp cache"
733 | & " [functionId:" & functionId & "]"
734 | & " [uuid:" & uuid & "]"
735 | & " [req:" & req & "]"
736 | & " [private:" & private & "]"
737 | & " [res:" & res & "]"
738 | & " [step:" & step & "]"
739 | & " [$~json.hash:" & $~json.hash & "]"
740 | & " [~hashPosition:" & ~hashPosition & "]"
741 | & " [~leftOfHash:" & ~leftOfHash & "]"
742 | & " [~valueNumber:" & ~valueNumber & "]"
743 | & " [~data:" & ~data & "]"
744 | ) disabled logging end */
745 | ] ;
746 | Evaluate ( ~data )
747 | ) ;
748 | /* else: use the data already in temporary cache */
749 | $~json.cache[1]
750 | ) ;
751 |
752 | /* object/array walking functions increment/decrement this value on every iteration, so setting it to 0 here causes it to start at 1 on the first value */
753 | $~json.depth = 0
754 | /* logging start disabled
755 | ; ~! = LogWriterMemoryCreateEntry (
756 | "GetValuePosition: initialize cache: end"
757 | & " [functionId:" & functionId & "]"
758 | & " [uuid:" & uuid & "]"
759 | & " [req:" & req & "]"
760 | & " [private:" & private & "]"
761 | & " [res:" & res & "]"
762 | & " [step:" & step & "]"
763 | & " [$~json.hash:" & $~json.hash & "]"
764 | & "¶[$~json.cache[1]:" & $~json.cache[1] & "]"
765 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
766 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
767 | ) disabled logging end */
768 | ] ; "" )
769 | )
770 | ] ;
771 | ""
772 | ) ;
773 |
774 |
775 |
776 | functionId = 199 ;
777 | /**
778 | * =====================================================================
779 | * Error
780 | *
781 | * parameters:
782 | * req = error message
783 | * =====================================================================
784 | */
785 | Let ( [
786 | /* *
787 | * am not using a standard logging start/end comment because error
788 | * logging should always be enabled
789 | */
790 | message = req ;
791 |
792 | /* logging start disabled
793 | ~! = LogWriterMemoryCreateEntry (
794 | "Error"
795 | & " [message:" & message & "]"
796 | ) ; disabled logging end */
797 |
798 | /* save the error */
799 | $json.error = List (
800 | message
801 | & If ( $~json.at > 0 ;
802 | " [ch:" & $~json.ch & "]"
803 | & " [at:" & $~json.at & "]"
804 | & " [context:"
805 | & Middle ( $~json.text ; $~json.at - 11 ; 20 )
806 | & "]"
807 | )
808 | ;
809 | $json.error
810 | ) ;
811 | /* trigger parent functions to abort */
812 | $~json.ch = "" ;
813 | $~json.at = -1
814 | ] ;
815 | /**
816 | * Don't return anything because calling function can always access
817 | * the error from $json.error, and many calling functions don't
818 | * need to access the error.
819 | */
820 | ""
821 | ) ;
822 |
823 |
824 | /**
825 | * =====================================================================
826 | * ELSE: FUNCTION NOT FOUND
827 | * =====================================================================
828 | */
829 | z_jsonParseSupport1 ( 199 /* Error */ ;
830 | "FunctionId [" & functionId & "]does not exist" ; "" ; "" ; ""
831 | )
832 | )
833 |
834 | /* logging start disabled
835 | )
836 | disabled logging end */
--------------------------------------------------------------------------------
/Functions/z_jsonParseSupport2.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * z_jsonParseSupport2 ( functionId ; req ; private ; res ; step )
4 | *
5 | * PURPOSE:
6 | * Supporting code for native json parsing functions.
7 | * mid-level/object walking/array walking
8 | *
9 | * PARAMETERS:
10 | * functionId = numeric code for function to run, in range of 200 - 299
11 | * 200 ParseObjectFindEnd
12 | * 201 ParseObjectFindValue
13 | * 202 ParseObjectGetKeyList
14 | * 210 ParseArrayFindEnd
15 | * 211 ParseArrayFindValue
16 | * 212 ParseArrayGetKeyList
17 | * 220 GetKeyListFromCache
18 | * 221 GetIndexListFromCache
19 | *
20 | * DEPENDENCIES:
21 | * Custom Functions:
22 | * z_jsonParseSupport1 - 3
23 | *
24 | * LICENSE:
25 | * See the LICENSE.md file for license rights and limitations (MIT):
26 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
27 | *
28 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
29 | *
30 | * HISTORY:
31 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
32 | * =====================================
33 | */
34 |
35 | /* logging start disabled
36 | Let ( [
37 | uuid = Get ( UUID ) ;
38 | ~! = LogWriterMemoryCreateEntry (
39 | "z_jsonParseSupport2"
40 | & " [functionId:" & functionId & "]"
41 | & " [uuid:" & uuid & "]"
42 | & " [req:" & req & "]"
43 | & " [private:" & private & "]"
44 | & " [res:" & res & "]"
45 | & " [step:" & step & "]"
46 | )
47 | ] ;
48 | disabled logging end */
49 | Case (
50 |
51 |
52 | functionId ≤ 212 ;
53 | /**
54 | * =====================================================================
55 | * STRUCTURE WALKING FUNCTIONS
56 | *
57 | * ParseObjectFindEnd: Move pointer to the end of the object.
58 | * ParseObjectFindValue: Move pointer to the start of the value for a specified key.
59 | * ParseObjectGetKeyList: Get a list of top-level keys in an object.
60 | *
61 | * parameters:
62 | * req = key to find for ParseObjectFindValue, not used for others
63 | * private = value list:
64 | * 1 = starting position of this object
65 | * 2 = current array index
66 | *
67 | * returns:
68 | * ParseObjectFindEnd: Empty string.
69 | * ParseObjectFindValue: Cache data if value is found, empty if it isn't.
70 | * ParseObjectGetKeyList: A list of top-level keys in an object.
71 | * =====================================================================
72 | */
73 | Let ( [
74 | /**
75 | ******************************************************
76 | * local vars used by any/all sections
77 | ******************************************************
78 | */
79 | ~cache.enabled = If ( $json.config.cache.enabled
80 | and $~json.depth ≤ $json.config.cache.maxdepth
81 | ;
82 | True
83 | ) ;
84 | ~index = GetValue ( private ; 2 ) ;
85 | ~index = If ( IsEmpty ( ~index ) ; 0 ; GetAsNumber ( ~index ) + 1 ) ;
86 | ~isObject = functionId < 210 ;
87 |
88 |
89 | /**
90 | ******************************************************
91 | * STEP 0: SET-UP
92 | ******************************************************
93 | */
94 | /* save the starting position of this object */
95 | ~startPos = If ( step < 1 ;
96 | $~json.at - 1 ;
97 | GetAsNumber ( GetValue ( private ; 1 ) )
98 | ) ;
99 | $~json.depth = If ( step < 1 ;
100 | $~json.depth + 1 ;
101 | $~json.depth
102 | ) ;
103 |
104 | ~! = If ( step < 1 ;
105 | /* function:next_with_param_no_result */
106 | If ( $~json.ch ≠ If ( ~isObject ; "{" ; "[" ) ;
107 | z_jsonParseSupport1 ( 199 /* Error */ ;
108 | "Pointer was not at start of structure" ; "" ; "" ; ""
109 | ) ;
110 | Let ( [
111 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
112 | $~json.at = $~json.at + 1
113 | ] ;
114 | ""
115 | )
116 | )
117 | ) ;
118 |
119 | /* READ CACHE */
120 | step = If ( step < 1
121 | and ~cache.enabled
122 | and not IsEmpty ( $~json.cache[$~json.depth] )
123 | and IsEmpty ( $json.error )
124 | ;
125 | /* logging start disabled
126 | LogWriterMemoryCreateEntry (
127 | "ParseStructure cache: read: start"
128 | & " [functionId:" & functionId & "]"
129 | & " [uuid:" & uuid & "]"
130 | & " [req:" & req & "]"
131 | & " [private:" & private & "]"
132 | & " [res:" & res & "]"
133 | & " [step:" & step & "]"
134 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
135 | ) & disabled logging end */
136 | Case (
137 | functionId = 201 /* ParseObjectFindValue */
138 | or functionId = 211 /* ParseArrayFindValue */
139 | ;
140 | Let ( [
141 | ~lastEntry = RightValues ( $~json.cache[$~json.depth] ; 1 ) ;
142 | ~cacheIsComplete = Left ( ~lastEntry ; 6 ) = "|DONE|" ;
143 | /**
144 | * search cache for the key
145 | * am surrounding the name with the record and column separator characters so
146 | * I'm sure to only find a full name match
147 | * this will prevent the name "base" from matching "baseball", for example
148 | */
149 | ~cachePosStart = Position (
150 | ¶ & $~json.cache[$~json.depth] ;
151 | ¶ & Substitute ( req ; [Char(13);""] ; [Char(10);""] ; ["|";""] ) & "|" ;
152 | 1 ;
153 | 1
154 | ) ;
155 | ~cachePosEnd = If ( ~cachePosStart ;
156 | Position ( $~json.cache[$~json.depth] & ¶ ; ¶ ; ~cachePosStart ; 1 )
157 | ) ;
158 | ~cache = If ( ~cachePosStart ;
159 | Middle (
160 | $~json.cache[$~json.depth] ;
161 | ~cachePosStart ;
162 | ~cachePosEnd - ~cachePosStart
163 | )
164 | )
165 | /* logging start disabled
166 | ; ~! = LogWriterMemoryCreateEntry (
167 | "ParseStructure cache: read: data"
168 | & " [functionId:" & functionId & "]"
169 | & " [uuid:" & uuid & "]"
170 | & " [req:" & req & "]"
171 | & " [private:" & private & "]"
172 | & " [res:" & res & "]"
173 | & " [step:" & step & "]"
174 | & " [~lastEntry:" & ~lastEntry & "]"
175 | & " [~cacheIsComplete:" & ~cacheIsComplete & "]"
176 | & " [~cachePosStart:" & ~cachePosStart & "]"
177 | & " [~cachePosEnd:" & ~cachePosEnd & "]"
178 | & " [~cache:" & ~cache & "]"
179 | ) disabled logging end */
180 | ] ;
181 | Case (
182 | /* key found in cache */
183 | /* TODO: do I need to update the value of $~json.at and $~json.ch ? */
184 | ~cachePosStart ;
185 | Let ( [
186 | /* cache stores position as an offset from the start of the object, undo that offset so the position is based on the offset from the start of the entire json string */
187 | ~! = If ( IsEmpty ( ~cache ) ;
188 | z_jsonParseSupport1 ( 199 /* Error */ ;
189 | "Invalid cache:" & Quote ( ~cache ) ; "" ; "" ; ""
190 | )
191 | ) ;
192 | ~cacheAsList = Substitute ( ~cache ; "|" ; ¶ ) ;
193 | ~keyStartPos = GetValue ( ~cacheAsList ; 2 ) ;
194 | ~valueStartPos = GetValue ( ~cacheAsList ; 4 ) ;
195 | ~valueEndPos = GetValue ( ~cacheAsList ; 5 ) ;
196 | /* NOTE: do not need to return hash */
197 | /* NOTE: applying an offset is not necessary unless I add a feature which allows accessing a specific value from a child object */
198 | ~cacheWithOffsetApplied =
199 | GetValue ( ~cacheAsList ; 1 )
200 | & "|" & If ( not IsEmpty ( ~keyStartPos ) ;
201 | GetAsNumber ( ~keyStartPos ) + ~startPos - 1
202 | )
203 | & "|" & GetValue ( ~cacheAsList ; 3 )
204 | & "|" & GetAsNumber ( ~valueStartPos ) + ~startPos - 1
205 | & "|" & GetAsNumber ( ~valueEndPos ) + ~startPos - 1
206 | ;
207 | $~json.temp.res = ~cacheWithOffsetApplied ;
208 | ~nextStep = 2 /* clean-up */
209 | /* logging start disabled
210 | ; ~! = LogWriterMemoryCreateEntry (
211 | "ParseStructure cache: read: found"
212 | & " [functionId:" & functionId & "]"
213 | & " [uuid:" & uuid & "]"
214 | & " [req:" & req & "]"
215 | & " [private:" & private & "]"
216 | & " [res:" & res & "]"
217 | & " [step:" & step & "]"
218 | & " [~nextStep:" & ~nextStep & "]"
219 | & " [~cacheWithOffsetApplied:" & ~cacheWithOffsetApplied & "]"
220 | ) disabled logging end */
221 | ] ;
222 | ~nextStep
223 | ) ;
224 |
225 | /* key does not exist */
226 | ~cacheIsComplete ;
227 | 2 ; /* nextStep: clean-up */
228 |
229 | /* else: search the object for the value, starting where cache left off */
230 | Let ( [
231 | ~length = Length ( ~lastEntry ) ;
232 | ~lastPipe = Position ( ~lastEntry ; "|" ; ~length ; -1 ) ;
233 | ~lastAt = Right ( ~lastEntry ; ~length - ~lastPipe ) ;
234 | $~json.temp.index = If ( ~isObject ; ~index ;
235 | /* load next index for an array */
236 | Left (
237 | ~lastEntry ;
238 | Position ( ~lastEntry ; "|" ; 1 ; 1 ) - 1
239 | ) + 1
240 | ) ;
241 | $~json.at = GetAsNumber ( ~lastAt ) + ~startPos ;
242 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 ) ;
243 | ~nextStep = 1 /* main */
244 | /* logging start disabled
245 | ; ~! = LogWriterMemoryCreateEntry (
246 | "ParseStructure cache: read: not in cache and cache not complete"
247 | & " [functionId:" & functionId & "]"
248 | & " [uuid:" & uuid & "]"
249 | & " [req:" & req & "]"
250 | & " [private:" & private & "]"
251 | & " [res:" & res & "]"
252 | & " [step:" & step & "]"
253 | & " [~length:" & ~length & "]"
254 | & " [~lastPipe:" & ~lastPipe & "]"
255 | & " [~lastAt:" & ~lastAt & "]"
256 | & " [$~json.temp.index:" & $~json.temp.index & "]"
257 | & " [~nextStep:" & ~nextStep & "]"
258 | ) disabled logging end */
259 | ] ;
260 | If ( IsEmpty ( ~lastAt ) ;
261 | 2 /* nextStep: clean-up */
262 | & z_jsonParseSupport1 ( 199 /* Error */ ;
263 | "Invalid 'lastAt' value in cache: " & Quote ( ~cache ) ; "" ; "" ; ""
264 | )
265 | ;
266 | ~nextStep
267 | )
268 | )
269 | )
270 | ) ;
271 |
272 | functionId = 202 /* ParseObjectGetKeyList */
273 | or functionId = 212 /* ParseArrayGetKeyList */
274 | ;
275 | Let ( [
276 | ~lastEntry = RightValues ( $~json.cache[$~json.depth] ; 1 ) ;
277 | ~cacheIsComplete = Left ( ~lastEntry ; 6 ) = "|DONE|" ;
278 | /**
279 | * get list of keys already cached
280 | */
281 | $~json.temp.res = If ( functionId = 202 ;
282 | z_jsonParseSupport2 ( 220 /* GetKeyListFromCache */ ; "" ; "" ; "" ; "" ) ;
283 | z_jsonParseSupport2 ( 221 /* GetIndexListFromCache */ ; "" ; "" ; "" ; "" )
284 | ) ;
285 | ~nextStep = If ( ~cacheIsComplete ; 2 ; 1 )
286 |
287 | /* logging start disabled
288 | ; ~! = LogWriterMemoryCreateEntry (
289 | "ParseStructure cache: read: data"
290 | & " [functionId:" & functionId & "]"
291 | & " [uuid:" & uuid & "]"
292 | & " [req:" & req & "]"
293 | & " [private:" & private & "]"
294 | & " [res:" & res & "]"
295 | & " [step:" & step & "]"
296 | & " [~lastEntry:" & ~lastEntry & "]"
297 | & " [~cacheIsComplete:" & ~cacheIsComplete & "]"
298 | & " [~nextStep:" & ~nextStep & "]"
299 | & " [$~json.temp.res:" & $~json.temp.res & "]"
300 | ) disabled logging end */
301 | ] ;
302 | If ( ~cacheIsComplete ;
303 | 2 ; /* nextStep: clean-up */
304 |
305 | /* else: move pointer to last position and parse the rest of the object */
306 | Let ( [
307 | $~json.temp.index = If ( ~isObject ; ~index ;
308 | /* load next index for an array */
309 | Left (
310 | ~lastEntry ;
311 | Position ( ~lastEntry ; "|" ; 1 ; 1 ) - 1
312 | ) + 1
313 | ) ;
314 | ~nextStep = 1 /* main */
315 |
316 | /* logging start disabled
317 | ; ~! = LogWriterMemoryCreateEntry (
318 | "ParseStructure cache: read: cache not complete"
319 | & " [functionId:" & functionId & "]"
320 | & " [uuid:" & uuid & "]"
321 | & " [req:" & req & "]"
322 | & " [private:" & private & "]"
323 | & " [res:" & res & "]"
324 | & " [step:" & step & "]"
325 | & " [$~json.temp.index:" & $~json.temp.index & "]"
326 | & " [~nextStep:" & ~nextStep & "]"
327 | ) disabled logging end */
328 | ] ;
329 | ~nextStep
330 | )
331 | )
332 | ) ;
333 |
334 | /* else: caching not enabled for this function, yet */
335 |
336 | /* logging start disabled
337 | LogWriterMemoryCreateEntry (
338 | "ParseStructure cache not enabled for this function"
339 | & " [functionId:" & functionId & "]"
340 | & " [uuid:" & uuid & "]"
341 | ) disabled logging end */
342 |
343 | ) ;
344 | /* else: don't attempt to read cache */
345 | step
346 | ) ;
347 |
348 | /* pass data up from above cache calculations to a root-level let variable */
349 | res = If ( ~cache.enabled and not IsEmpty ( $~json.temp.res ) ;
350 | Let ( [ res = $~json.temp.res ; $~json.temp.res = "" ] ; res ) ;
351 | res
352 | ) ;
353 | ~index = If ( ~cache.enabled and not IsEmpty ( $~json.temp.index ) ;
354 | Let ( [ ~index = $~json.temp.index ; $~json.temp.index = "" ] ; ~index ) ;
355 | ~index
356 | ) ;
357 |
358 |
359 | ~! = If ( step < 1
360 | and $~json.ch ≤ " "
361 | ;
362 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
363 | ) ;
364 | /* check if object is empty */
365 | ~isEmpty = If ( step < 1
366 | and IsEmpty ( $json.error )
367 | and $~json.ch = If ( ~isObject ; "}" ; "]" )
368 | ;
369 | True &
370 | /* function:next_no_result */
371 | Let ( [
372 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
373 | $~json.at = $~json.at + 1
374 | ] ;
375 | ""
376 | )
377 | ) ;
378 | ~endFound = ~isEmpty ;
379 | res = If ( step < 1 ;
380 | Case (
381 | functionId = 200 /* ParseObjectFindEnd */
382 | or functionId = 210 /* ParseArrayFindEnd */
383 | ;
384 | "" ;
385 |
386 | functionId = 201 ; /* ParseObjectFindValue */
387 | If ( IsEmpty ( req )
388 | or ~endFound
389 | ;
390 | False
391 | ) ;
392 |
393 | functionId = 211 ; /* ParseArrayFindValue */
394 | /* index values start at 0, so anything lower is invalid */
395 | If ( req < 0
396 | or ~endFound
397 | ;
398 | False
399 | ) ;
400 |
401 | functionId = 202 /* ParseObjectGetKeyList */
402 | or functionId = 212 /* ParseArrayGetKeyList */
403 | ;
404 | res
405 | ) ;
406 | res
407 | ) ;
408 | /* logging start disabled
409 | ~! = If ( step < 1 ;
410 | LogWriterMemoryCreateEntry (
411 | "ParseStructure setup"
412 | & " [functionId:" & functionId & "]"
413 | & " [uuid:" & uuid & "]"
414 | & " [req:" & req & "]"
415 | & " [private:" & private & "]"
416 | & " [res:" & res & "]"
417 | & " [step:" & step & "]"
418 | & " [~endFound:" & ~endFound & "]"
419 | )
420 | ) ; disabled logging end */
421 | step = If ( step < 1 ;
422 | If (
423 | not IsEmpty ( $json.error )
424 | or ~endFound
425 | or not IsEmpty ( res )
426 | ;
427 | 2 ; /* clean-up */
428 | 1 /* main */
429 | ) ;
430 | step
431 | ) ;
432 |
433 | /**
434 | ******************************************************
435 | * STEP 1: MAIN
436 | ******************************************************
437 | */
438 | ~keyStartPos = If ( step = 1 and ~isObject ;
439 | $~json.at - 1
440 | ) ;
441 | ~key = If ( step = 1 ;
442 | If ( ~isObject ;
443 | z_jsonParseSupport3 ( 310 /* ParseString */ ; "" ; "" ; "" ; "" ) ;
444 | ~index
445 | )
446 | ) ;
447 | ~valueFound = If ( step = 1
448 | and (
449 | functionId = 201 /* ParseObjectFindValue */
450 | or functionId = 211 /* ParseArrayFindValue */
451 | ) ;
452 | ~key = req
453 | ) ;
454 |
455 | /* logging start disabled
456 | ~! = If ( step = 1 and ~valueFound ;
457 | LogWriterMemoryCreateEntry (
458 | "ParseStructure main: valueFound"
459 | & " [functionId:" & functionId & "]"
460 | & " [uuid:" & uuid & "]"
461 | & " [req:" & req & "]"
462 | & " [private:" & private & "]"
463 | & " [res:" & res & "]"
464 | & " [step:" & step & "]"
465 | & " [~key:" & ~key & "]"
466 | & " [~valueFound:" & ~valueFound & "]"
467 | )
468 | ) ; disabled logging end */
469 |
470 | ~! = If ( step = 1 and ~isObject
471 | and $~json.ch ≤ " "
472 | ;
473 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
474 | ) ;
475 | ~! = If ( step = 1 and ~isObject ;
476 | /* function:next_with_param_no_result */
477 | If ( $~json.ch ≠ ":" ;
478 | z_jsonParseSupport1 ( 199 /* Error */ ;
479 | "Expected : instead of " & $~json.ch ; "" ; "" ; ""
480 | ) ;
481 | Let ( [
482 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
483 | $~json.at = $~json.at + 1
484 | ] ;
485 | ""
486 | )
487 | )
488 | ) ;
489 | ~! = If ( step = 1
490 | and $~json.ch ≤ " "
491 | and ~isObject
492 | ;
493 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
494 | ) ;
495 | ~valueType = If ( step = 1 ;
496 | Case (
497 | $~json.ch = "{" ; "o" ;
498 | $~json.ch = "[" ; "a" ;
499 | $~json.ch = "\"" ; "s" ;
500 | $~json.ch = "-" or ( $~json.ch ≥ 0 and $~json.ch ≤ 9 ) ; "n" ;
501 | /* else: assume word */
502 | "w"
503 | )
504 | ) ;
505 | ~valStartPos = If ( step = 1 ;
506 | $~json.at - 1
507 | ) ;
508 | ~value = If ( step = 1 and IsEmpty ( $json.error ) ;
509 | If ( ~cache.enabled and ( ~valueType = "o" or ~valueType = "a" );
510 | /* parse and return the value, so it can be hashed, for caching */
511 | z_jsonParseSupport1 ( 100 /* ParseValue */ ; "" ; "" ; "" ; "" ) ;
512 | /* else: don't need the value */
513 | z_jsonParseSupport1 ( 101 /* ParseValueSkip */ ; "" ; "" ; "" ; "" )
514 | )
515 | ) ;
516 | ~valEndPos = If ( step = 1 ;
517 | $~json.at - 2
518 | ) ;
519 | ~! = If ( step = 1 and $~json.ch ≤ " " ;
520 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
521 | ) ;
522 | /* check if at end of structure */
523 | ~endFound = If ( step = 1
524 | and IsEmpty ( $json.error )
525 | and $~json.ch = If ( ~isObject ; "}" ; "]" )
526 | ;
527 | True &
528 | /* function:next_no_result */
529 | Let ( [
530 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
531 | $~json.at = $~json.at + 1
532 | ] ;
533 | ""
534 | )
535 | ;
536 | /* else: don't change it's value, it may have been set in setup */
537 | ~endFound
538 | ) ;
539 | /* advance pointer to start of next value */
540 | ~! = If ( step = 1
541 | and IsEmpty ( $json.error )
542 | and not ~endFound
543 | ;
544 | /* function:next_with_param_no_result */
545 | If ( $~json.ch ≠ "," ;
546 | z_jsonParseSupport1 ( 199 /* Error */ ;
547 | "Expected \",\" instead of " & Quote ( $~json.ch ) ; "" ; "" ; ""
548 | ) ;
549 | Let ( [
550 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
551 | $~json.at = $~json.at + 1
552 | ] ;
553 | ""
554 | )
555 | )
556 | & If ( $~json.ch ≤ " " ;
557 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; "" )
558 | )
559 | ) ;
560 | /*
561 | * add this value to cache
562 | */
563 | $~json.cache[$~json.depth] = If ( step = 1
564 | and IsEmpty ( $json.error )
565 | and ~cache.enabled
566 | ;
567 | /* logging start disabled
568 | LogWriterMemoryCreateEntry (
569 | "ParseStructure main: before adding value to cache"
570 | & " [functionId:" & functionId & "]"
571 | & " [uuid:" & uuid & "]"
572 | & " [req:" & req & "]"
573 | & " [private:" & private & "]"
574 | & " [res:" & res & "]"
575 | & " [step:" & step & "]"
576 | & " [~index:" & ~index & "]"
577 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
578 | ) & disabled logging end */
579 | List ( $~json.cache[$~json.depth] ;
580 | Substitute ( ~key ; [Char(13);""] ; [Char(10);""] ; ["|";""] )
581 | & "|" & If ( ~isObject ; ~keyStartPos - ~startPos + 1 )
582 | & "|" & ~valueType
583 | & "|" & ~valStartPos - ~startPos + 1
584 | & "|" & ~valEndPos - ~startPos + 1
585 | & "|" & /* hash */ If ( ~valueType = "o" or ~valueType = "a" ;
586 | GetContainerAttribute ( ~value ; "MD5" )
587 | )
588 | & "|" & $~json.at - ~startPos - If ( ~endFound ; 1 )
589 | ) ;
590 |
591 | /* else */
592 | $~json.cache[$~json.depth]
593 | /* logging start disabled
594 | & LogWriterMemoryCreateEntry (
595 | "ParseStructure main: value NOT added to cache"
596 | & " [functionId:" & functionId & "]"
597 | & " [uuid:" & uuid & "]"
598 | & " [req:" & req & "]"
599 | & " [private:" & private & "]"
600 | & " [res:" & res & "]"
601 | & " [step:" & step & "]"
602 | & " [~index:" & ~index & "]"
603 | & " [~cache.enabled:" & ~cache.enabled & "]"
604 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
605 | ) disabled logging end */
606 | ) ;
607 |
608 | res = If ( step = 1 ;
609 | Case (
610 | functionId = 200 /* ParseObjectFindEnd */
611 | or functionId = 210 /* ParseArrayFindEnd */
612 | ;
613 | "" ;
614 |
615 | functionId = 201 /* ParseObjectFindValue */
616 | or functionId = 211 /* ParseArrayFindValue */
617 | ;
618 | If ( ~valueFound ;
619 | /* returned data is almost the same as the format used in cache, except the position values are not offset and it does not include hash */
620 | Substitute ( ~key ; [Char(13);""] ; [Char(10);""] ; ["|";""] )
621 | & "|" & If ( ~isObject ; ~keyStartPos )
622 | & "|" & ~valueType
623 | & "|" & ~valStartPos
624 | & "|" & ~valEndPos
625 | ) ;
626 |
627 | functionId = 202 /* ParseObjectGetKeyList */
628 | or functionId = 212 /* ParseArrayGetKeyList */
629 | ;
630 | List ( res ; ~key )
631 | ) ;
632 | res
633 | ) ;
634 |
635 | /* logging start disabled
636 | ~! = If ( step = 1 ;
637 | LogWriterMemoryCreateEntry (
638 | "ParseStructure main"
639 | & " [functionId:" & functionId & "]"
640 | & " [uuid:" & uuid & "]"
641 | & " [req:" & req & "]"
642 | & " [private:" & private & "]"
643 | & " [res:" & res & "]"
644 | & " [step:" & step & "]"
645 | & " [~key:" & ~key & "]"
646 | & " [~valueFound:" & ~valueFound & "]"
647 | & " [~endFound:" & ~endFound & "]"
648 | & " [~keyStartPos:" & ~keyStartPos & "]"
649 | & " [~valueType:" & ~valueType & "]"
650 | & " [~valStartPos:" & ~valStartPos & "]"
651 | & " [~valEndPos:" & ~valEndPos & "]"
652 | & " [~index:" & ~index & "]"
653 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
654 | )
655 | ) ; disabled logging end */
656 |
657 | step = If ( step = 1 ;
658 | If (
659 | not IsEmpty ( $json.error )
660 | or ~endFound
661 | or ~valueFound
662 | ;
663 | 2 ; /* clean-up */
664 | step /* recurse */
665 | ) ;
666 | step
667 | ) ;
668 |
669 | /**
670 | ******************************************************
671 | * ELSE: CLEAN-UP
672 | ******************************************************
673 | */
674 | res = If ( step = 2 ;
675 | If ( not IsEmpty ( $json.error ) ;
676 | "?" ;
677 | res
678 | ) ;
679 | res
680 | ) ;
681 | /* SAVE CACHE */
682 | ~! = If ( step = 2
683 | and IsEmpty ( $json.error )
684 | and ~cache.enabled
685 | and ~endFound
686 | and not ~isEmpty
687 | ;
688 | Let ( [
689 | ~value = Middle ( $~json.text ; ~startPos ; $~json.at - ~startPos - 1 ) ;
690 | ~hash = GetContainerAttribute ( ~value ; "MD5" ) ;
691 | /* logging start disabled
692 | ~! = LogWriterMemoryCreateEntry (
693 | "ParseStructure clean-up: save cache"
694 | & " [functionId:" & functionId & "]"
695 | & " [hash:" & ~hash & "]"
696 | & " [value:" & ~value & "]"
697 | ) ; disabled logging end */
698 | /* is there any scenario where this data already exists in cache?
699 | TODO: try to write a test that makes this code fail by adding the same hash/data to stored cache more than once.
700 | */
701 | $~json.cache.hash = List ( $~json.cache.hash ; ~hash ) ;
702 | $~json.cache[$~json.depth] = List ( $~json.cache[$~json.depth] ; "|DONE|" ) ;
703 | ~data = Quote ( $~json.cache[$~json.depth] ) ;
704 | $~json.cache[$~json.depth] = If ( $~json.depth > 1 ;
705 | /* clear temp cache from this depth, so it can be used by a new structure */
706 | "" ;
707 | /* don't clear root cache; it will be cleared when initializing the cache, if it needs to be */
708 | $~json.cache[$~json.depth]
709 | ) ;
710 | $~json.cache.data = List ( $~json.cache.data ; ~data )
711 | ] ; "" )
712 | ) ;
713 | /* load separate values back into private */
714 | private = ~startPos & ¶ & ~index ;
715 | /* logging start disabled
716 | ~! = If ( step = 2 ;
717 | LogWriterMemoryCreateEntry (
718 | "ParseStructure clean-up"
719 | & " [functionId:" & functionId & "]"
720 | & " [uuid:" & uuid & "]"
721 | & " [req:" & req & "]"
722 | & " [private:" & private & "]"
723 | & " [res:" & res & "]"
724 | & " [step:" & step & "]"
725 | & " [~key:" & ~key & "]"
726 | & " [~valueFound:" & ~valueFound & "]"
727 | & " [~endFound:" & ~endFound & "]"
728 | & " [~startPos:" & ~startPos & "]"
729 | & "¶[$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
730 | & "¶[$~json.cache.hash:" & $~json.cache.hash & "]"
731 | & "¶[$~json.cache.data:" & $~json.cache.data & "]"
732 | )
733 | ) ; disabled logging end */
734 | $~json.depth = If ( step = 2 ;
735 | $~json.depth - 1 ;
736 | $~json.depth
737 | )
738 | ] ;
739 | If ( step = 1 ;
740 | z_jsonParseSupport2 ( functionId ; req ; private ; res ; step ) ;
741 | res
742 | )
743 | ) ;
744 |
745 |
746 |
747 | functionId = 220 /* GetKeyListFromCache */
748 | or functionId = 221 /* GetIndexListFromCache */
749 | ;
750 | /**
751 | * =====================================================================
752 | * GetIndexListFromCache
753 | *
754 | * parameters:
755 | * req = not used
756 | * private = iteration counter
757 | *
758 | * GetKeyListFromCache returns:
759 | * list of object names for data in $~json.cache[$~json.depth]
760 | *
761 | * GetIndexListFromCache returns:
762 | * list of array indexes for data in $~json.cache[$~json.depth]
763 | * =====================================================================
764 | */
765 | Let ( [
766 | /**
767 | ******************************************************
768 | * local vars used by any/all sections
769 | ******************************************************
770 | */
771 | /* ~i is a 1 based iterator used to extract one value at a time from cache */
772 | ~i = If ( IsEmpty ( private ) ; 1 ; GetAsNumber ( private ) + 1 ) ;
773 | ~cacheItem = GetValue ( $~json.cache[$~json.depth] ; ~i ) ;
774 | ~cacheIsComplete = Left ( ~cacheItem ; 6 ) = "|DONE|" ;
775 | step = If (
776 | IsEmpty ( ~cacheItem ) or ~cacheIsComplete ;
777 | 2 ; /* main */
778 | 1 /* clean-up*/
779 | ) ;
780 | /* logging start disabled
781 | ~! = LogWriterMemoryCreateEntry (
782 | "GetKeyOrIndexListFromCache setup on every call"
783 | & " [functionId:" & functionId & "]"
784 | & " [uuid:" & uuid & "]"
785 | & " [private:" & private & "]"
786 | & " [res:" & res & "]"
787 | & " [step:" & step & "]"
788 | & " [~i:" & ~i & "]"
789 | & " [~cacheItem:" & ~cacheItem & "]"
790 | & " [~cacheIsComplete:" & ~cacheIsComplete & "]"
791 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
792 | ) ; disabled logging end */
793 |
794 | /**
795 | ******************************************************
796 | * STEP 0: SET-UP
797 | ******************************************************
798 | */
799 |
800 |
801 | /**
802 | ******************************************************
803 | * STEP 1: MAIN
804 | ******************************************************
805 | */
806 | ~key = If ( step = 1 ;
807 | If ( functionId = 220 /* GetKeyListFromCache */ ;
808 | /**
809 | * extract original key from json
810 | * (the key in cache has had returns and pipe removed)
811 | */
812 | Let ( [
813 | ~keyStartPos = GetValue ( Substitute ( ~cacheItem ; "|" ; "¶" ) ; 2 ) ;
814 | /* NOTE: am NOT offsetting by the array's start position because GetKeyList is only meant to be called on a top-level structure */
815 | $~json.at = GetAsNumber ( ~keyStartPos ) + 1 ;
816 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
817 | ] ;
818 | z_jsonParseSupport3 ( 310 /* ParseString */ ; "" ; "" ; "" ; "" )
819 | ) ;
820 |
821 | /* else: GetIndexListFromCache */
822 | Left (
823 | ~cacheItem ;
824 | Position ( ~cacheItem ; "|" ; 1 ; 1 ) - 1
825 | )
826 | )
827 | ) ;
828 | ~! = If ( step = 1
829 | and functionId = 221 /* GetIndexListFromCache */
830 | and GetAsNumber ( ~key ) ≠ ~i - 1
831 | ;
832 | z_jsonParseSupport1 ( 199 /* Error */ ;
833 | "Invalid cache: key " & Quote ( ~key ) & " did not match index " & Quote ( ~i - 1 ) ; "" ; "" ; ""
834 | )
835 | ) ;
836 | res = If ( step = 1 and IsEmpty ( $~json.error ) ;
837 | List ( res ; ~key ) ;
838 | res
839 | ) ;
840 |
841 | /* logging start disabled
842 | ~! = If ( step = 1 ; LogWriterMemoryCreateEntry (
843 | "GetKeyOrIndexListFromCache after main"
844 | & " [functionId:" & functionId & "]"
845 | & " [uuid:" & uuid & "]"
846 | & " [private:" & private & "]"
847 | & " [res:" & res & "]"
848 | & " [step:" & step & "]"
849 | & " [~i:" & ~i & "]"
850 | & " [~cacheItem:" & ~cacheItem & "]"
851 | & " [~cacheIsComplete:" & ~cacheIsComplete & "]"
852 | & " [~key:" & ~key & "]"
853 | ) ) ; disabled logging end */
854 |
855 |
856 | /**
857 | ******************************************************
858 | * ELSE: CLEAN-UP
859 | ******************************************************
860 | */
861 | step = If ( not IsEmpty ( $json.error ) ; 2 ; step ) ;
862 |
863 | res = If ( step = 2 ;
864 | If ( not IsEmpty ( $json.error ) ;
865 | "?" ;
866 | res
867 | ) ;
868 | res
869 | ) ;
870 |
871 | ~! = If ( step = 2 ;
872 | /* move pointer to last processed character */
873 | Let ( [
874 | ~lastCacheData = GetValue ( $~json.cache[$~json.depth] ; ~i - 1 ) ;
875 | ~length = Length ( ~lastCacheData ) ;
876 | ~lastPipe = Position ( ~lastCacheData ; "|" ; ~length ; -1 ) ;
877 | ~lastAt = Right ( ~lastCacheData ; ~length - ~lastPipe ) ;
878 | /* NOTE: am NOT offsetting by the array's start position because GetKeyList is only meant to be called on a top-level structure */
879 | $~json.at = GetAsNumber ( ~lastAt ) + 1 ;
880 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
881 | /* logging start disabled
882 | ; ~! = If ( step = 2 ; LogWriterMemoryCreateEntry (
883 | "GetKeyOrIndexListFromCache clean-up: move pointer"
884 | & " [functionId:" & functionId & "]"
885 | & " [uuid:" & uuid & "]"
886 | & " [private:" & private & "]"
887 | & " [res:" & res & "]"
888 | & " [step:" & step & "]"
889 | & " [~i:" & ~i & "]"
890 | & " [~lastCacheData:" & ~lastCacheData & "]"
891 | & " [~length:" & ~length & "]"
892 | & " [~lastPipe:" & ~lastPipe & "]"
893 | & " [~lastAt:" & ~lastAt & "]"
894 | & " [$~json.cache[" & $~json.depth & "]:" & $~json.cache[$~json.depth] & "]"
895 | ) ) disabled logging end */
896 | ] ;
897 | If ( IsEmpty ( ~lastAt ) ;
898 | z_jsonParseSupport1 ( 199 /* Error */ ;
899 | "Invalid 'lastAt' value in cache: " & Quote ( ~lastCacheData ) ; "" ; "" ; ""
900 | )
901 | )
902 | )
903 | ) ;
904 |
905 | /* load data back into private */
906 | private = ~i
907 |
908 | /* logging start disabled
909 | ; ~! = If ( step = 2 ; LogWriterMemoryCreateEntry (
910 | "GetKeyOrIndexListFromCache after clean-up"
911 | & " [functionId:" & functionId & "]"
912 | & " [uuid:" & uuid & "]"
913 | & " [private:" & private & "]"
914 | & " [res:" & res & "]"
915 | & " [step:" & step & "]"
916 | & " [~i:" & ~i & "]"
917 | & " [~cacheItem:" & ~cacheItem & "]"
918 | ) ) disabled logging end */
919 |
920 | ] ;
921 | If ( step = 1 ;
922 | z_jsonParseSupport2 ( functionId ; req ; private ; res ; step ) ;
923 | res
924 | )
925 | ) ;
926 |
927 |
928 | /**
929 | * =====================================================================
930 | * ELSE: FUNCTION NOT FOUND
931 | * =====================================================================
932 | */
933 | z_jsonParseSupport1 ( 199 /* Error */ ;
934 | "FunctionId [" & functionId & "]does not exist" ; "" ; "" ; ""
935 | )
936 | )
937 |
938 | /* logging start disabled
939 | )
940 | disabled logging end */
--------------------------------------------------------------------------------
/Functions/z_jsonParseSupport3.fmfn:
--------------------------------------------------------------------------------
1 | /**
2 | * =====================================
3 | * z_jsonParseSupport3 ( functionId ; req ; private ; res ; step )
4 | *
5 | * PURPOSE:
6 | * Supporting code for native json parsing functions.
7 | * low-level/specific value parsing
8 | *
9 | * PARAMETERS:
10 | * functionId = numeric code for function to run, in range of 300 - 399
11 | * 300 ParseWhitespace
12 | * 310 ParseString
13 | * 311 ParseHex
14 | * 320 ParseNumber
15 | * 321 ParseDigits
16 | * 330 ParseWord
17 | * req = requested data (if any)
18 | * private = private data, likely sent to recursive calls of a function
19 | * res = response
20 | * step = current state of a recursive function
21 | *
22 | * DEPENDENCIES:
23 | * Custom Functions:
24 | * z_jsonParseSupport1, 3
25 | *
26 | * LICENSE:
27 | * See the LICENSE.md file for license rights and limitations (MIT):
28 | * https://raw.githubusercontent.com/dansmith65/FileMaker-JSON-Functions/master/LICENSE.md
29 | *
30 | * LINK: https://github.com/dansmith65/FileMaker-JSON-Functions
31 | *
32 | * HISTORY:
33 | * v1.0.0 RELEASED on 2015-APR-27 by Daniel Smith dansmith65@gmail.com
34 | * =====================================
35 | */
36 |
37 | /* logging start disabled
38 | Let ( [
39 | uuid = Get ( UUID ) ;
40 | ~! = LogWriterMemoryCreateEntry (
41 | "z_jsonParseSupport3"
42 | & " [functionId:" & functionId & "]"
43 | & " [uuid:" & uuid & "]"
44 | & " [req:" & req & "]"
45 | & " [private:" & private & "]"
46 | & " [res:" & res & "]"
47 | & " [step:" & step & "]"
48 | )
49 | ] ;
50 | disabled logging end */
51 | Case (
52 |
53 |
54 |
55 | functionId = 300 ;
56 | /**
57 | * =====================================================================
58 | * ParseWhitespace
59 | *
60 | * Skip whitespace.
61 | *
62 | * parameters:
63 | * req = not used
64 | * private = not used
65 | *
66 | * returns:
67 | * Empty string.
68 | *
69 | * notes:
70 | * Whitespace characters are those listed on wikipedia that are less than
71 | * or equal to a space character Char(32). This isn't strictly the same
72 | * as testing if the character is less than or equal to a space, but I
73 | * think it's sufficient for most use-cases.
74 | * http://en.wikipedia.org/wiki/Whitespace_character
75 | * =====================================================================
76 | */
77 | If ( not IsEmpty ( $~json.ch ) and $~json.ch ≤ " " ;
78 | Let ( [
79 | step = step + 1 ;
80 | ~chunkSize = 10 ;
81 | ~chunk = Middle ( $~json.text ; $~json.at - 1 ; ~chunkSize ) ;
82 | ~stripped = Substitute ( ~chunk ; [Char(9);""]; [Char(10);""]; [Char(11);""]; [Char(12);""]; [Char(13);""]; [Char(32);""] ) ;
83 | ~firstCh = Left ( ~stripped ; 1 )
84 | /* logging start disabled
85 | ; ~! = LogWriterMemoryCreateEntry (
86 | "ParseWhitespace"
87 | & " [step:" & step & "]"
88 | & " [~chunk:" & ~chunk & "]"
89 | & " [~stripped:" & ~stripped & "]"
90 | & " [~firstCh:" & ~firstCh & "]"
91 | ) disabled logging end */
92 | ] ;
93 | If ( IsEmpty ( ~firstCh ) ;
94 | /* this chunk has all be stripped */
95 | Let ( [
96 | $~json.at = $~json.at + ~chunkSize
97 | ] ;
98 | z_jsonParseSupport3 ( 300 /* ParseWhitespace */ ; "" ; "" ; "" ; step )
99 | ) ;
100 |
101 | /* else: move pointer to start of first non stripped character */
102 | Let ( [
103 | ~pos = Position ( ~chunk ; ~firstCh ; 1 ; 1 ) ;
104 | $~json.at = $~json.at + ~pos - 1 ;
105 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
106 | /* logging start disabled
107 | ; ~! = LogWriterMemoryCreateEntry (
108 | "ParseWhitespace end"
109 | & " [step:" & step & "]"
110 | & " [~pos:" & ~pos & "]"
111 | ) disabled logging end */
112 | ] ;
113 | "" /* end recursion */
114 | )
115 | )
116 | ) ;
117 | "" /* end recursion */
118 | /* logging start disabled
119 | & LogWriterMemoryCreateEntry (
120 | "ParseWhitespace end without moving pointer"
121 | ) disabled logging end */
122 | ) ;
123 |
124 |
125 |
126 | functionId = 310 ;
127 | /**
128 | * =====================================================================
129 | * ParseString
130 | *
131 | * Extract an decode a json string.
132 | *
133 | * parameters:
134 | * req = not used
135 | * private = iteration counter (for debugging)
136 | *
137 | * returns:
138 | * FileMaker text (the original value before it was encoded as json).
139 | * =====================================================================
140 | */
141 | Let ( [
142 | private = private + 1 ;
143 | ~end = Position (
144 | $~json.text ;
145 | "\"" ;
146 | $~json.at ; /* start */
147 | 1 /* occurrence */
148 | ) ;
149 | ~escapePos = Position (
150 | $~json.text ;
151 | "\\" ;
152 | $~json.at ; /* start */
153 | 1 /* occurrence */
154 | )
155 | ] ;
156 | Case (
157 | ~end = 0 ;
158 | z_jsonParseSupport1 ( 199 /* Error */ ;
159 | "Bad string: reached end of json before end of string" ; "" ; "" ; ""
160 | ) & "?" ;
161 |
162 | ~escapePos > 0 and ~escapePos < ~end ;
163 | /* the escape character was found */
164 | Let ( [
165 | /* logging start disabled
166 | ~! = LogWriterMemoryCreateEntry (
167 | "z_jsonParseSupport3 ParseString escape found"
168 | & " [private:" & private & "]"
169 | & " [res:" & res & "]"
170 | & " [~end:" & ~end & "]"
171 | & " [~escapePos:" & ~escapePos & "]"
172 | ) ; disabled logging end */
173 | /* extract data from pointer to escape character */
174 | ~string = Middle (
175 | $~json.text ;
176 | $~json.at ;
177 | ~escapePos - $~json.at
178 | ) ;
179 | /* update pointer */
180 | $~json.at = ~escapePos + 1 ;
181 | ~! = /* function:next_no_result */
182 | Let ( [
183 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
184 | $~json.at = $~json.at + 1
185 | ] ;
186 | ""
187 | )
188 | ;
189 | /* unescape the character */
190 | ~ch = Case (
191 | $~json.ch = "u" ; z_jsonParseSupport3 ( 311 /* ParseHex */ ; "" ; "" ; "" ; "" ) ;
192 | $~json.ch = "\"" ; "\"" ;
193 | $~json.ch = "\\" ; "\\" ;
194 | $~json.ch = "/" ; "/" ;
195 | $~json.ch = "b" ; Char ( 8 ) ;
196 | $~json.ch = "f" ; Char ( 12 ) ;
197 | $~json.ch = "n" ; Char ( 10 ) ;
198 | $~json.ch = "r" ; Char ( 13 ) ;
199 | $~json.ch = "t" ; Char ( 9 ) ;
200 | /* else */
201 | z_jsonParseSupport1 ( 199 /* Error */ ;
202 | "Bad string: invalid value after backslash escape" ; "" ; "" ; ""
203 | )
204 | )
205 | /* logging start disabled
206 | ; ~! = LogWriterMemoryCreateEntry (
207 | "z_jsonParseSupport3 ParseString"
208 | & " [private:" & private & "]"
209 | & " [~string:" & ~string & "]"
210 | & " [~ch:" & ~ch & "]"
211 | & " [res:" & res & ~string & ~ch & "]"
212 | & " [~end:" & ~end & "]"
213 | & " [~escapePos:" & ~escapePos & "]"
214 | ) disabled logging end */
215 | ] ;
216 | z_jsonParseSupport3 ( 310 /* ParseString */ ;
217 | "" ;
218 | private ;
219 | res & ~string & ~ch ;
220 | ""
221 | )
222 | ) ;
223 |
224 | /* else: end recursion */
225 | Let ( [
226 | ~string = Middle ( $~json.text ; $~json.at ; ~end - $~json.at ) ;
227 | /* update pointer */
228 | $~json.at = ~end + 2 ;
229 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
230 | /* logging start disabled
231 | ; ~! = LogWriterMemoryCreateEntry (
232 | "z_jsonParseSupport3 ParseString end"
233 | & " [private:" & private & "]"
234 | & " [res:" & res & ~string & "]"
235 | & " [~end:" & ~end & "]"
236 | & " [~escapePos:" & ~escapePos & "]"
237 | ) disabled logging end */
238 | ] ;
239 | If ( not IsEmpty ( $json.error ) ; /* an error occurred */
240 | "?" ;
241 | res & ~string
242 | )
243 | )
244 | )
245 | ) ;
246 |
247 |
248 |
249 |
250 | functionId = 311 ;
251 | /**
252 | * =====================================================================
253 | * ParseHex
254 | *
255 | * While parsing a string, convert an escaped hex code to a unicode value.
256 | *
257 | * parameters:
258 | * req = not used
259 | * private = internal use only; iteration counter
260 | *
261 | * returns:
262 | * Unicode character.
263 | * =====================================================================
264 | */
265 | Let ( [
266 | private = private + 1 ;
267 | ~ch = /* function:next */
268 | Let ( [
269 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
270 | $~json.at = $~json.at + 1
271 | ] ;
272 | $~json.ch
273 | )
274 | ;
275 | ~hex = Position (
276 | "0123456789ABCDEF" ;
277 | ~ch ;
278 | 1 ;
279 | 1
280 | ) - 1 ;
281 | res = res * 16 + ~hex
282 | ] ;
283 | Case (
284 | not IsEmpty ( $json.error ) ; /* an error occurred */
285 | "" ;
286 |
287 | IsEmpty ( ~hex )
288 | or ~hex < 0
289 | or ~hex > 15
290 | or private = 4
291 | ;
292 | Char ( res ) ; /* return the unicode character */
293 |
294 | z_jsonParseSupport3 ( 311 /* ParseHex */ ; "" ; private ; res ; step )
295 | )
296 | ) ;
297 |
298 |
299 |
300 | functionId = 320 ;
301 | /**
302 | * =====================================================================
303 | * ParseNumber
304 | *
305 | * While parsing a string, convert an escaped hex code to a unicode value.
306 | *
307 | * parameters:
308 | * req = not used
309 | * private = not used
310 | *
311 | * returns:
312 | * Number
313 | * =====================================================================
314 | */
315 | Let ( [
316 | ~string =
317 | If ( $~json.ch = "-" ;
318 | "-"
319 | /* function:next_no_result */
320 | & Let ( [
321 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
322 | $~json.at = $~json.at + 1
323 | ] ;
324 | ""
325 | )
326 | )
327 |
328 | & z_jsonParseSupport3 ( 321 /*ParseDigits*/ ; "" ; "" ; "" ; "" )
329 |
330 | & If ( $~json.ch = "." ;
331 | "."
332 | /* function:next_no_result */
333 | & Let ( [
334 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
335 | $~json.at = $~json.at + 1
336 | ] ;
337 | ""
338 | )
339 | & z_jsonParseSupport3 ( 321 /*ParseDigits*/ ; "" ; "" ; "" ; "" )
340 | )
341 |
342 | & If ( $~json.ch = "e" ;
343 | $~json.ch
344 | /* function:next_no_result */
345 | & Let ( [
346 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
347 | $~json.at = $~json.at + 1
348 | ] ;
349 | ""
350 | )
351 | & If ( $~json.ch = "-" or $~json.ch = "+" ;
352 | $~json.ch
353 | /* function:next_no_result */
354 | & Let ( [
355 | $~json.ch = Middle ( $~json.text ; $~json.at ; 1 ) ;
356 | $~json.at = $~json.at + 1
357 | ] ;
358 | ""
359 | )
360 | )
361 | )
362 |
363 | & z_jsonParseSupport3 ( 321 /*ParseDigits*/ ; "" ; "" ; "" ; "" )
364 | ] ;
365 | If ( IsEmpty ( $json.error ) ;
366 | GetAsNumber ( ~string )
367 | )
368 | ) ;
369 |
370 |
371 |
372 | functionId = 321 ;
373 | /**
374 | * =====================================================================
375 | * ParseDigits
376 | *
377 | * Extract a stream of digits
378 | *
379 | * parameters:
380 | * req = not used
381 | * private = not used
382 | *
383 | * returns:
384 | * a stream of digits as text
385 | * =====================================================================
386 | */
387 | If ( $~json.ch ≥ "0" and $~json.ch ≤ "9" ;
388 | Let ( [
389 | ~chunkSize = 10 ;
390 | ~chunk = Middle ( $~json.text ; $~json.at - 1 ; ~chunkSize ) ;
391 | ~noDigits = Substitute ( ~chunk ; [0;""]; [1;""]; [2;""]; [3;""]; [4;""]; [5;""]; [6;""]; [7;""]; [8;""]; [9;""] ) ;
392 | ~firstNonDigit = Left ( ~noDigits ; 1 )
393 | ] ;
394 | If ( IsEmpty ( ~firstNonDigit ) ;
395 | /* this chunk is all digits */
396 | Let ( [
397 | $~json.at = $~json.at + ~chunkSize
398 | ] ;
399 | z_jsonParseSupport3 ( 321 /*ParseDigits*/ ; "" ; "" ; res & ~chunk ; "" )
400 | ) ;
401 |
402 | /* else: move pointer to start of first non digit */
403 | Let ( [
404 | ~pos = Position ( ~chunk ; ~firstNonDigit ; 1 ; 1 ) ;
405 | ~digits = Middle ( ~chunk ; 1 ; ~pos - 1 ) ;
406 | $~json.at = $~json.at + ~pos - 1 ;
407 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
408 | ] ;
409 | res & ~digits
410 | )
411 | )
412 | ) ;
413 | res
414 | ) ;
415 |
416 |
417 |
418 | functionId = 330 ;
419 | /**
420 | * =====================================================================
421 | * ParseWord
422 | *
423 | * Extract a stream of digits
424 | *
425 | * parameters:
426 | * req = not used
427 | * private = not used
428 | *
429 | * returns:
430 | * a stream of digits as text
431 | * =====================================================================
432 | */
433 | If ( $~json.ch ≠ "t" and $~json.ch ≠ "f" and $~json.ch ≠ "n" ;
434 | z_jsonParseSupport1 ( 199 /* Error */ ;
435 | "Unexpected " & Quote ( $~json.ch ) & " at start of ParseWord" ; "" ; "" ; ""
436 | )
437 | & "?" ;
438 |
439 | Let ( [
440 | ~len = Case (
441 | $~json.ch = "t" ; 4 ;
442 | $~json.ch = "f" ; 5 ;
443 | $~json.ch = "n" ; 4
444 | ) ;
445 | ~word = Middle ( $~json.text ; $~json.at - 1 ; ~len ) ;
446 | res = Case (
447 | ~word = "true" ; True ;
448 | ~word = "false" ; False ;
449 | ~word = "null" ; "json:null"
450 | )
451 | ] ;
452 | If ( IsEmpty ( res ) ;
453 | z_jsonParseSupport1 ( 199 /* Error */ ;
454 | "Invalid word " & Quote ( ~word ) ; "" ; "" ; ""
455 | )
456 | & "?" ;
457 |
458 | /* else: update pointer, return word */
459 | Let ( [
460 | $~json.at = $~json.at + ~len ;
461 | $~json.ch = Middle ( $~json.text ; $~json.at - 1 ; 1 )
462 | ] ;
463 | res
464 | )
465 | )
466 | )
467 | ) ;
468 |
469 |
470 | /**
471 | * =====================================================================
472 | * ELSE: FUNCTION NOT FOUND
473 | * =====================================================================
474 | */
475 | z_jsonParseSupport1 ( 199 /* Error */ ;
476 | "FunctionId [" & functionId & "]does not exist" ; "" ; "" ; ""
477 | )
478 | )
479 |
480 | /* logging start disabled
481 | )
482 | disabled logging end */
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dan Smith
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##WHAT
2 |
3 | A set of recursive [FileMaker](http://www.filemaker.com/) custom functions that can create and read JSON.
4 |
5 | When reading JSON, utilizes cache in $local variables to improve the speed of reading more than one value. The cache also allows for reading from JSON that is too large to be read in a single pass (due to FileMaker's max of 50,000 recursive calls).
6 |
7 |
8 | ##WHY
9 |
10 | My previous [FileMaker-JSON](https://github.com/dansmith65/FileMaker-JSON) project was script based and used [Let Notation](http://filemakerstandards.org/pages/viewpage.action?pageId=5668879) as an intermediary format. While this method worked, it introduced the overhead of an intermediary format that may not be desirable if your intention is to create JSON to send to a web service, or parse the response from a web service. That method also relies on evaluating text as code, which introduces a security issue that may be unacceptable in certain circumstances.
11 |
12 | ##HOW TO INSTALL
13 |
14 | Copy all functions from [FileMaker-JSON-Functions.fmp12](FMFiles/FileMaker-JSON-Functions.fmp12), except LogWriterMemoryCreateEntry, then paste them into your own file.
15 |
16 | NOTE: `jsonA` and `jsonO` are included in that file, but not in this project because they are an exact copy of the functions with the same name from [geistinteractive/JSONCustomFunctions](https://github.com/geistinteractive/JSONCustomFunctions).
17 |
18 | ##HOW TO USE
19 |
20 | Refer to the Test Expression field in [FileMaker-JSON-Functions.fmp12](FMFiles/FileMaker-JSON-Functions.fmp12) for example code. There are examples of using similar functions here: https://www.geistinteractive.com/docs/fmqbo/working-with-json/
21 |
22 | ##WHO
23 |
24 | I'd like to thank [geist interactive](https://www.geistinteractive.com/) for sponsoring this project. I've wanted to work in it for a while now, but with out the sponsorship, I'm not sure when I would have gotten around to it.
25 |
26 | ##STATUS
27 |
28 | Stable (as far as I know) first release. There are currently ~180 test to verify these functions work as expected.
29 |
30 | My goal is to match the functionality of the [BaseElements](http://www.goya.com.au/baseelements/plugin) backed set of custom functions with the same name available at [geistinteractive/JSONCustomFunctions](https://github.com/geistinteractive/JSONCustomFunctions). This project is much slower than the BaseElements backed functions, so if the BaseElements plugin is available to you, those functions are preferred.
31 |
32 | ## License
33 |
34 | See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
35 |
--------------------------------------------------------------------------------
/recursion_notes.md:
--------------------------------------------------------------------------------
1 | Recursion Notes
2 | ===============
3 |
4 | This file serves as a collection of observations I've made on writing recursive custom functions in FileMaker.
5 |
6 |
7 | FileMaker Documentation
8 | -----------------------
9 |
10 | Recursion limit for custom function:
11 |
12 | * 50,000 recursive calls total
13 | * The call stack is only allowed to be 10,000 calls deep at any point. If these limits are violated, the custom function will return "?".
14 | * Note that tail-recursion is properly optimized, so tail calls do not increase the size of the call stack.
15 |
16 |
17 | Observations
18 | ------------
19 |
20 | ###### Tail recursion allows a maximum depth of 49,999.
21 |
22 | ```
23 | /**
24 | * test ( iteration )
25 | *
26 | * test ( 0 ) = "?"
27 | * test ( 1 ) = 49999
28 | */
29 |
30 | If ( iteration >= 49999 ;
31 | iteration ;
32 | test ( iteration + 1 )
33 | )
34 | ```
35 |
36 |
37 | ###### Saving the result to a variable prevents tail recursion and limits the maximum iterations to 10,000.
38 |
39 | ```
40 | /**
41 | * test ( iteration )
42 | *
43 | * test ( 0 ) = "?"
44 | * test ( 1 ) = 10000
45 | */
46 |
47 | If ( iteration >= 10000 ;
48 | iteration ;
49 | Let ( [
50 | result = test ( iteration + 1 )
51 | ] ;
52 | result
53 | )
54 | )
55 | ```
56 |
57 |
58 | ###### letVariables, $localVariables, and $$globalVariables can still be used with tail recursion, though.
59 |
60 | ```
61 | /**
62 | * test ( iteration )
63 | *
64 | * test ( 0 ) = "?"
65 | * test ( 1 ) = 49999
66 | */
67 |
68 | If ( iteration >= 49999 ;
69 | iteration ;
70 | Let ( [
71 | letVaraible = iteration ;
72 | $localVariable = letVaraible ;
73 | $$globalVariable = $localVariable
74 | ] ;
75 | test ( $$globalVariable + 1 )
76 | )
77 | )
78 | ```
79 |
80 |
81 | ###### Nested Let statements can be used with tail recursion as well.
82 |
83 | ```
84 | /**
85 | * test ( iteration )
86 | *
87 | * test ( 0 ) = "?"
88 | * test ( 1 ) = 49999
89 | */
90 |
91 | If ( iteration >= 49999 ;
92 | iteration ;
93 | Let ( [
94 | letVaraible = iteration
95 | ] ;
96 | Let ( [
97 | $localVariable = letVaraible
98 | ] ;
99 | Let ( [
100 | $$globalVariable = $localVariable
101 | ] ;
102 | test ( $$globalVariable + 1 )
103 | )
104 | )
105 | )
106 | )
107 | ```
108 |
109 |
110 | ###### A recursive sub-function does not "free up" it's recursive calls once it returns it's result to the calling function. In other words; there is no way to go beyond 50,000 recursive calls.
111 |
112 | ```
113 | /**
114 | * test ( iteration )
115 | *
116 | * test ( 0 ) = "?"
117 | * test ( 1 ) = 49991
118 | */
119 |
120 | If ( iteration >= 49991 ;
121 | iteration ;
122 | test ( iteration + 1 + testSub ( 1 ) )
123 | )
124 |
125 | /**
126 | * testSub ( iteration )
127 | */
128 |
129 | If ( iteration >= 4998 ;
130 | iteration ;
131 | testSub ( iteration + 1 )
132 | )
133 | ```
134 |
135 |
136 | ###### Non-recursive sub-function also uses up recursive calls.
137 |
138 | ```
139 | /**
140 | * test ( iteration )
141 | *
142 | * test ( 0 ) = "?"
143 | * test ( 1 ) = 25000
144 | */
145 |
146 | If ( iteration >= 25000 ;
147 | iteration ;
148 | test ( testSub ( iteration ) + 1 )
149 | )
150 |
151 | /**
152 | * testSub ( iteration )
153 | * (all this function does is return it's parameter)
154 | */
155 |
156 | iteration
157 | ```
158 |
159 |
160 | ###### The more sub-functions you access, the less recursion depth you get in the calling function.
161 |
162 | ```
163 | /**
164 | * test ( iteration )
165 | *
166 | * test ( 0 ) = "?"
167 | * test ( 1 ) = 16667
168 | */
169 |
170 | If ( iteration >= 16667 ;
171 | iteration ;
172 | test (
173 | testSub ( iteration )
174 | + testSub ( 1 )
175 | )
176 | )
177 |
178 | /**
179 | * testSub ( iteration )
180 | * (all this function does is return it's parameter)
181 | */
182 |
183 | iteration
184 | ```
185 |
186 | ```
187 | /**
188 | * test ( iteration )
189 | *
190 | * test ( 0 ) = "?"
191 | * test ( 1 ) = 6250
192 | */
193 |
194 | If ( iteration >= 6250 ;
195 | iteration ;
196 | test (
197 | testSub ( iteration )
198 | + testSub ( 1 )
199 | + testSub ( 0 )
200 | + testSub ( 0 )
201 | + testSub ( 0 )
202 | + testSub ( 0 )
203 | + testSub ( 0 )
204 | )
205 | )
206 |
207 | /**
208 | * testSub ( iteration )
209 | * (all this function does is return it's parameter)
210 | */
211 |
212 | iteration
213 | ```
214 |
215 |
216 | ###### Be careful how sub-functions are referenced, or they will prevent tail recursion:
217 |
218 | ```
219 | /**
220 | * test ( iteration )
221 | *
222 | * test ( 0 ) = "?"
223 | * test ( 1 ) = 10000
224 | */
225 |
226 | If ( iteration >= 10000 ;
227 | iteration ;
228 | test ( iteration + 1 ) & testSub ( "" )
229 | )
230 |
231 | /**
232 | * testSub ( iteration )
233 | * (all this function does is return it's parameter)
234 | */
235 |
236 | iteration
237 | ```
238 |
239 |
240 | ###### Sub function calling the parent can use tail recursion.
241 |
242 | ```
243 | /**
244 | * test ( iteration )
245 | *
246 | * test ( 0 ) = "?"
247 | * test ( 1 ) = 25000
248 | */
249 |
250 | If ( iteration >= 25000 ;
251 | iteration ;
252 | testSub ( iteration + 1 )
253 | )
254 |
255 | /**
256 | * testSub ( iteration )
257 | *
258 | */
259 |
260 | test ( iteration )
261 | ```
262 |
--------------------------------------------------------------------------------