├── .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 | --------------------------------------------------------------------------------