diff --git a/Dockerfile b/Dockerfile index f4fbe9a..0bb15a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN addgroup -S duster && adduser -S duster -G duster # Install LinkedIn DustJS. Installing it globally before copying in # the repo to avoid spamming the npm servers with every codebase # change. -RUN npm install -g dustjs-linkedin +RUN npm install -g dustjs-linkedin dustjs-helpers # Copy repo into duster user's directory @@ -22,5 +22,5 @@ RUN chown -R duster:duster /home/duster/duster USER duster RUN git clean -dfxq RUN cargo build -RUN npm link dustjs-linkedin +RUN npm link dustjs-linkedin dustjs-helpers ENTRYPOINT ["/home/duster/duster/js/run_compliance_suite.bash"] diff --git a/js/dustjs_shim.js b/js/dustjs_shim.js index 4e02180..c38f06d 100644 --- a/js/dustjs_shim.js +++ b/js/dustjs_shim.js @@ -1,20 +1,21 @@ -var dust = require('dustjs-linkedin'); -var fs = require('fs'); -const path = require('path'); +// var dust = require("dustjs-linkedin"); +var dust = require("dustjs-helpers"); +var fs = require("fs"); +const path = require("path"); var argv = process.argv.slice(2); if (argv.length < 1) { console.error("Expecting only 1 argument (a path to a template)"); process.exit(1); } -var context = JSON.parse(fs.readFileSync(0, 'utf-8')); +var context = JSON.parse(fs.readFileSync(0, "utf-8")); var main_template = path.parse(argv[0])["name"]; for (var i = 0, len = argv.length; i < len; ++i) { var filename = path.parse(argv[i])["name"]; try { - var template_source = fs.readFileSync(argv[i], 'utf-8'); + var template_source = fs.readFileSync(argv[i], "utf-8"); } catch (err) { console.error(err); process.exit(1); diff --git a/js/run_compliance_suite.bash b/js/run_compliance_suite.bash index 4423242..c7d5296 100755 --- a/js/run_compliance_suite.bash +++ b/js/run_compliance_suite.bash @@ -38,7 +38,7 @@ while read -r test_group; do else echo "$test_group_name::$test_case_name FAILED" if [ $show_diff -eq 1 ]; then - diff --label "dustjs-linked" --label "duster" <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case" 2>/dev/null ) <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case" 2>/dev/null ) + diff --label "dustjs-linkedin" --label "duster" <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case" 2>/dev/null ) <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case" 2>/dev/null ) fi exit 1 fi @@ -48,7 +48,12 @@ while read -r test_group; do fi set -e done <<<"$(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.json' | sort)" -done <<<"$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d | sort)" +done <<<"$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d ! -name '_*' | sort)" + +ignored_count=$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d -name '_*' | wc -l) + +echo "" +echo "$ignored_count ignored tests" if [ $failed_count -ne 0 ]; then echo "$failed_count failed tests" diff --git a/js/test_cases/helpers_eq/README.md b/js/test_cases/helpers_eq/README.md new file mode 100644 index 0000000..ddcf8e1 --- /dev/null +++ b/js/test_cases/helpers_eq/README.md @@ -0,0 +1,15 @@ +Without a key parameter, neither the main block nor the else block is rendered. + +Literal values work in both keys and values. + +Can't Walk Theory +----------------- + +Assuming a missing value = cantwalk and a non-existent key = cantwalk then their equality makes sense. + +The null tests have proven that absent parameters and missing values do not equal null and therefore it is not just making all falsey values equal. + +Non-Scalar Values +----------------- + +For non-scalar values, it seems that dust does mark two values as true if they have the same path, but otherwise they are not equal. diff --git a/js/test_cases/helpers_eq/input1.json b/js/test_cases/helpers_eq/input1.json new file mode 100644 index 0000000..bfbbd94 --- /dev/null +++ b/js/test_cases/helpers_eq/input1.json @@ -0,0 +1,60 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_eq/main.dust b/js/test_cases/helpers_eq/main.dust new file mode 100644 index 0000000..c1b8562 --- /dev/null +++ b/js/test_cases/helpers_eq/main.dust @@ -0,0 +1,28 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@eq key=str value="master"}str is equal to "master"{:else}str does not equal "master"{/eq}{~n} +{@eq key=str value="7"}str is equal to "7"{:else}str does not equal "7"{/eq}{~n} +{@eq key=int value="7"}int is equal to "7"{:else}int does not equal "7"{/eq}{~n} +{@eq key=int value=7}int is equal to 7{:else}int does not equal 7{/eq}{~n} +{@eq key=alpha value=beta}alpha is equal to beta{:else}alpha does not equal beta{/eq}{~n} +{@eq value=beta}missing key is true{:else}missing key is false{/eq}{~n} +{@eq value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/eq}{~n} +{@eq key=alpha}missing value is true{:else}missing value is false{/eq}{~n} +{@eq key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/eq}{~n} +{@eq key="master" value="master"}"master" is equal to "master"{:else}"master" does not equal "master"{/eq}{~n} +{@eq key=null}null equals a missing value{:else}null does not equal a missing value{/eq}{~n} +{@eq key=null value=gamma}null equals a non-existent value{:else}null does not equal a non-existent value{/eq}{~n} +{@eq}no parameters is true{:else}no parameters is false{/eq}{~n} +{@eq key=array_lower value=array_higher}[3,5,7] is equal to [8,9]{:else}[3,5,7] does not equal [8,9]{/eq}{~n} +{! non-scalar and copied value tests !} +{@eq key=array_lower value=array_lower}array_lower is equal to array_lower{:else}array_lower does not equal array_lower{/eq}{~n} +{@eq key=array_lower value=copy_array_lower}array_lower is equal to copy_array_lower{:else}array_lower does not equal copy_array_lower{/eq}{~n} +{@eq key=some_obj value=some_obj}some_obj is equal to some_obj{:else}some_obj does not equal some_obj{/eq}{~n} +{@eq key=some_obj value=copy_some_obj}some_obj is equal to copy_some_obj{:else}some_obj does not equal copy_some_obj{/eq}{~n} +{@eq key=some_obj value=other_obj}some_obj is equal to other_obj{:else}some_obj does not equal other_obj{/eq}{~n} +{@eq key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is equal to array_of_some_obj{:else}array_of_some_obj does not equal array_of_some_obj{/eq}{~n} +{@eq key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is equal to copy_array_of_some_obj{:else}array_of_some_obj does not equal copy_array_of_some_obj{/eq}{~n} +{@eq key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is equal to array_of_other_obj{:else}array_of_some_obj does not equal array_of_other_obj{/eq}{~n} diff --git a/js/test_cases/helpers_gt/README.md b/js/test_cases/helpers_gt/README.md new file mode 100644 index 0000000..527bae3 --- /dev/null +++ b/js/test_cases/helpers_gt/README.md @@ -0,0 +1,37 @@ +Based on my tests, it appears dust is sorting based on ascii-table values. This also appears to extend to unicode codepoints based on a symbols test. + +Greater than follows the same pattern for not rendering when key is omitted or null. + +Longer arrays are greater than shorter arrays if all preceding values match. + +Theory for comparing arrays: Compare the values, if theres any mismatched types then take the same action you would for non-matching scalar types. + +greater than +------------ + +All comparisons between non-matching types (for example, int vs string) appear to render the else block. + +Comparisons between non-scalar types (like arrays) appears to render the else block + + +greater than or equals to +------------------------- + +All comparisons between non-matching types (for example, int vs string) appear to render the main block. + +Comparisons between non-scalar types (like arrays) appears to render the main block + +less than +--------- + +All comparisons between non-matching types (for example, int vs string) appear to render the else block. + +Comparisons between non-scalar types (like arrays) appears to render the else block + + +less than or equal to +--------------------- + +All comparisons between non-matching types (for example, int vs string) appear to render the main block. + +Comparisons between non-scalar types (like arrays) appears to render the main block diff --git a/js/test_cases/helpers_gt/input1.json b/js/test_cases/helpers_gt/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_gt/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_gt/main.dust b/js/test_cases/helpers_gt/main.dust new file mode 100644 index 0000000..0bd9fca --- /dev/null +++ b/js/test_cases/helpers_gt/main.dust @@ -0,0 +1,42 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@gt key=str value="master"}str is greater than "master"{:else}str is less than or equal to "master"{/gt}{~n} +{@gt key=str value="7"}str is greater than "7"{:else}str is less than or equal to "7"{/gt}{~n} +{@gt key=int value="7"}int is greater than "7"{:else}int is less than or equal to "7"{/gt}{~n} +{@gt key=int value=7}int is greater than 7{:else}int is less than or equal to 7{/gt}{~n} +{@gt key=int value=6}int is greater than 6{:else}int is less than or equal to 6{/gt}{~n} +{@gt key=alpha value=beta}alpha is greater than beta{:else}alpha is less than or equal to beta{/gt}{~n} +{@gt value=beta}missing key is true{:else}missing key is false{/gt}{~n} +{@gt value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/gt}{~n} +{@gt key=alpha}missing value is true{:else}missing value is false{/gt}{~n} +{@gt key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/gt}{~n} +{@gt key="master" value="master"}"master" is greater than "master"{:else}"master" is less than or equal to "master"{/gt}{~n} +{@gt key=null}null is greater than a missing value{:else}null is less than or equal to a missing value{/gt}{~n} +{@gt key=null value=gamma}null is greater than a non-existent value{:else}null is less than or equal to a non-existent value{/gt}{~n} +{@gt}no parameters is true{:else}no parameters is false{/gt}{~n} +{@gt key="a" value="]"}"a" is greater than "]"{:else}"a" is less than or equal to "]"{/gt}{~n} +{@gt key="a" value="A"}"a" is greater than "A"{:else}"a" is less than or equal to "A"{/gt}{~n} +{@gt key="a" value="}"}"a" is greater than "}"{:else}"a" is less than or equal to "}"{/gt}{~n} +{! +Commented out because unicode breaks nom +{@gt key="☃" value="☄"}"☃" is greater than "☄"{:else}"☃" is less than or equal to "☄"{/gt}{~n} +!} +{@gt key=true_value value=false_value}true is greater than false{:else}true is less than or equal to false{/gt}{~n} +{@gt key=array_lower value=array_higher}[3,5,7] is greater than [8,9]{:else}[3,5,7] is less than or equal to [8,9]{/gt}{~n} +{@gt key=array_higher value=array_lower}[8,9] is greater than [3,5,7]{:else}[8,9] is less than or equal to [3,5,7]{/gt}{~n} +{! non-scalar and copied value tests !} +{@gt key=array_lower value=array_lower}array_lower is greater than array_lower{:else}array_lower is less than or equal to array_lower{/gt}{~n} +{@gt key=array_lower value=copy_array_lower}array_lower is greater than copy_array_lower{:else}array_lower is less than or equal to copy_array_lower{/gt}{~n} +{@gt key=some_obj value=some_obj}some_obj is greater than some_obj{:else}some_obj is less than or equal to some_obj{/gt}{~n} +{@gt key=some_obj value=copy_some_obj}some_obj is greater than copy_some_obj{:else}some_obj is less than or equal to copy_some_obj{/gt}{~n} +{@gt key=some_obj value=other_obj}some_obj is greater than other_obj{:else}some_obj is less than or equal to other_obj{/gt}{~n} +{@gt key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is greater than array_of_some_obj{:else}array_of_some_obj is less than or equal to array_of_some_obj{/gt}{~n} +{@gt key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is greater than copy_array_of_some_obj{:else}array_of_some_obj is less than or equal to copy_array_of_some_obj{/gt}{~n} +{@gt key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is greater than array_of_other_obj{:else}array_of_some_obj is less than or equal to array_of_other_obj{/gt}{~n} + +{@gt key=array_higher value=array_higher_longer}array_higher is greater than array_higher_longer{:else}array_higher is less than or equal to array_higher_longer{/gt}{~n} +{@gt key=array_lower value=array_lower_with_array}array_lower is greater than array_lower_with_array{:else}array_lower is less than or equal to array_lower_with_array{/gt}{~n} +{@gt key=array_lower_with_array value=array_lower}array_lower_with_array is greater than array_lower{:else}array_lower_with_array is less than or equal to array_lower{/gt}{~n} diff --git a/js/test_cases/helpers_gte/input1.json b/js/test_cases/helpers_gte/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_gte/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_gte/main.dust b/js/test_cases/helpers_gte/main.dust new file mode 100644 index 0000000..b0ee031 --- /dev/null +++ b/js/test_cases/helpers_gte/main.dust @@ -0,0 +1,41 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@gte key=str value="master"}str is greater than or equal to "master"{:else}str is less than "master"{/gte}{~n} +{@gte key=str value="7"}str is greater than or equal to "7"{:else}str is less than "7"{/gte}{~n} +{@gte key=int value="7"}int is greater than or equal to "7"{:else}int is less than "7"{/gte}{~n} +{@gte key=int value=7}int is greater than or equal to 7{:else}int is less than 7{/gte}{~n} +{@gte key=int value=6}int is greater than or equal to 6{:else}int is less than 6{/gte}{~n} +{@gte key=alpha value=beta}alpha is greater than or equal to beta{:else}alpha is less than beta{/gte}{~n} +{@gte value=beta}missing key is true{:else}missing key is false{/gte}{~n} +{@gte value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/gte}{~n} +{@gte key=alpha}missing value is true{:else}missing value is false{/gte}{~n} +{@gte key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/gte}{~n} +{@gte key="master" value="master"}"master" is greater than or equal to "master"{:else}"master" is less than "master"{/gte}{~n} +{@gte key=null}null is greater than or equal to a missing value{:else}null is less than a missing value{/gte}{~n} +{@gte key=null value=gamma}null is greater than or equal to a non-existent value{:else}null is less than a non-existent value{/gte}{~n} +{@gte}no parameters is true{:else}no parameters is false{/gte}{~n} +{@gte key="a" value="]"}"a" is greater than or equal to "]"{:else}"a" is less than "]"{/gte}{~n} +{@gte key="a" value="A"}"a" is greater than or equal to "A"{:else}"a" is less than "A"{/gte}{~n} +{@gte key="a" value="}"}"a" is greater than or equal to "}"{:else}"a" is less than "}"{/gte}{~n} +{! +Commented out because unicode breaks nom +{@gte key="☃" value="☄"}"☃" is greater than or equal to "☄"{:else}"☃" is less than "☄"{/gte}{~n} +!} +{@gte key=true_value value=false_value}true is greater than or equal to false{:else}true is less than false{/gte}{~n} +{@gte key=array_lower value=array_higher}[3,5,7] is greater than or equal to [8,9]{:else}[3,5,7] is less than [8,9]{/gte}{~n} +{@gte key=array_higher value=array_lower}[8,9] is greater than or equal to [3,5,7]{:else}[8,9] is less than [3,5,7]{/gte}{~n} +{! non-scalar and copied value tests !} +{@gte key=array_lower value=array_lower}array_lower is greater than or equal to array_lower{:else}array_lower is less than array_lower{/gte}{~n} +{@gte key=array_lower value=copy_array_lower}array_lower is greater than or equal to copy_array_lower{:else}array_lower is less than copy_array_lower{/gte}{~n} +{@gte key=some_obj value=some_obj}some_obj is greater than or equal to some_obj{:else}some_obj is less than some_obj{/gte}{~n} +{@gte key=some_obj value=copy_some_obj}some_obj is greater than or equal to copy_some_obj{:else}some_obj is less than copy_some_obj{/gte}{~n} +{@gte key=some_obj value=other_obj}some_obj is greater than or equal to other_obj{:else}some_obj is less than other_obj{/gte}{~n} +{@gte key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is greater than or equal to array_of_some_obj{:else}array_of_some_obj is less than array_of_some_obj{/gte}{~n} +{@gte key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is greater than or equal to copy_array_of_some_obj{:else}array_of_some_obj is less than copy_array_of_some_obj{/gte}{~n} +{@gte key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is greater than or equal to array_of_other_obj{:else}array_of_some_obj is less than array_of_other_obj{/gte}{~n} +{@gte key=array_higher value=array_higher_longer}array_higher is greater than or equal to array_higher_longer{:else}array_higher is less than array_higher_longer{/gte}{~n} +{@gte key=array_lower value=array_lower_with_array}array_lower is greater than or equal to array_lower_with_array{:else}array_lower is less than array_lower_with_array{/gte}{~n} +{@gte key=array_lower_with_array value=array_lower}array_lower_with_array is greater than or equal to array_lower{:else}array_lower_with_array is less than array_lower{/gte}{~n} diff --git a/js/test_cases/helpers_lt/input1.json b/js/test_cases/helpers_lt/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_lt/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_lt/main.dust b/js/test_cases/helpers_lt/main.dust new file mode 100644 index 0000000..f2b81d1 --- /dev/null +++ b/js/test_cases/helpers_lt/main.dust @@ -0,0 +1,41 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@lt key=str value="master"}str is less than "master"{:else}str is greater than or equal to "master"{/lt}{~n} +{@lt key=str value="7"}str is less than "7"{:else}str is greater than or equal to "7"{/lt}{~n} +{@lt key=int value="7"}int is less than "7"{:else}int is greater than or equal to "7"{/lt}{~n} +{@lt key=int value=7}int is less than 7{:else}int is greater than or equal to 7{/lt}{~n} +{@lt key=int value=6}int is less than 6{:else}int is greater than or equal to 6{/lt}{~n} +{@lt key=alpha value=beta}alpha is less than beta{:else}alpha is greater than or equal to beta{/lt}{~n} +{@lt value=beta}missing key is true{:else}missing key is false{/lt}{~n} +{@lt value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/lt}{~n} +{@lt key=alpha}missing value is true{:else}missing value is false{/lt}{~n} +{@lt key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/lt}{~n} +{@lt key="master" value="master"}"master" is less than "master"{:else}"master" is greater than or equal to "master"{/lt}{~n} +{@lt key=null}null is less than a missing value{:else}null is greater than or equal to a missing value{/lt}{~n} +{@lt key=null value=gamma}null is less than a non-existent value{:else}null is greater than or equal to a non-existent value{/lt}{~n} +{@lt}no parameters is true{:else}no parameters is false{/lt}{~n} +{@lt key="a" value="]"}"a" is less than "]"{:else}"a" is greater than or equal to "]"{/lt}{~n} +{@lt key="a" value="A"}"a" is less than "A"{:else}"a" is greater than or equal to "A"{/lt}{~n} +{@lt key="a" value="}"}"a" is less than "}"{:else}"a" is greater than or equal to "}"{/lt}{~n} +{! +Commented out because unicode breaks nom +{@lt key="☃" value="☄"}"☃" is less than "☄"{:else}"☃" is greater than or equal to "☄"{/lt}{~n} +!} +{@lt key=true_value value=false_value}true is less than false{:else}true is greater than or equal to false{/lt}{~n} +{@lt key=array_lower value=array_higher}[3,5,7] is less than [8,9]{:else}[3,5,7] is greater than or equal to [8,9]{/lt}{~n} +{@lt key=array_higher value=array_lower}[8,9] is less than [3,5,7]{:else}[8,9] is greater than or equal to [3,5,7]{/lt}{~n} +{! non-scalar and copied value tests !} +{@lt key=array_lower value=array_lower}array_lower is less than array_lower{:else}array_lower is greater than or equal to array_lower{/lt}{~n} +{@lt key=array_lower value=copy_array_lower}array_lower is less than copy_array_lower{:else}array_lower is greater than or equal to copy_array_lower{/lt}{~n} +{@lt key=some_obj value=some_obj}some_obj is less than some_obj{:else}some_obj is greater than or equal to some_obj{/lt}{~n} +{@lt key=some_obj value=copy_some_obj}some_obj is less than copy_some_obj{:else}some_obj is greater than or equal to copy_some_obj{/lt}{~n} +{@lt key=some_obj value=other_obj}some_obj is less than other_obj{:else}some_obj is greater than or equal to other_obj{/lt}{~n} +{@lt key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is less than array_of_some_obj{:else}array_of_some_obj is greater than or equal to array_of_some_obj{/lt}{~n} +{@lt key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is less than copy_array_of_some_obj{:else}array_of_some_obj is greater than or equal to copy_array_of_some_obj{/lt}{~n} +{@lt key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is less than array_of_other_obj{:else}array_of_some_obj is greater than or equal to array_of_other_obj{/lt}{~n} +{@lt key=array_higher value=array_higher_longer}array_higher is less than array_higher_longer{:else}array_higher is greater than or equal to array_higher_longer{/lt}{~n} +{@lt key=array_lower value=array_lower_with_array}array_lower is less than array_lower_with_array{:else}array_lower is greater than or equal to array_lower_with_array{/lt}{~n} +{@lt key=array_lower_with_array value=array_lower}array_lower_with_array is less than array_lower{:else}array_lower_with_array is greater than or equal to array_lower{/lt}{~n} diff --git a/js/test_cases/helpers_lte/input1.json b/js/test_cases/helpers_lte/input1.json new file mode 100644 index 0000000..de2ac09 --- /dev/null +++ b/js/test_cases/helpers_lte/input1.json @@ -0,0 +1,72 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "array_lower_with_array": [ + 3, + 5, + [ + 7 + ] + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "array_higher_longer": [ + 8, + 9, + 1 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_lte/main.dust b/js/test_cases/helpers_lte/main.dust new file mode 100644 index 0000000..72ee337 --- /dev/null +++ b/js/test_cases/helpers_lte/main.dust @@ -0,0 +1,41 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@lte key=str value="master"}str is less than or equal to "master"{:else}str is greater than "master"{/lte}{~n} +{@lte key=str value="7"}str is less than or equal to "7"{:else}str is greater than "7"{/lte}{~n} +{@lte key=int value="7"}int is less than or equal to "7"{:else}int is greater than "7"{/lte}{~n} +{@lte key=int value=7}int is less than or equal to 7{:else}int is greater than 7{/lte}{~n} +{@lte key=int value=6}int is less than or equal to 6{:else}int is greater than 6{/lte}{~n} +{@lte key=alpha value=beta}alpha is less than or equal to beta{:else}alpha is greater than beta{/lte}{~n} +{@lte value=beta}missing key is true{:else}missing key is false{/lte}{~n} +{@lte value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/lte}{~n} +{@lte key=alpha}missing value is true{:else}missing value is false{/lte}{~n} +{@lte key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/lte}{~n} +{@lte key="master" value="master"}"master" is less than or equal to "master"{:else}"master" is greater than "master"{/lte}{~n} +{@lte key=null}null is less than or equal to a missing value{:else}null is greater than a missing value{/lte}{~n} +{@lte key=null value=gamma}null is less than or equal to a non-existent value{:else}null is greater than a non-existent value{/lte}{~n} +{@lte}no parameters is true{:else}no parameters is false{/lte}{~n} +{@lte key="a" value="]"}"a" is less than or equal to "]"{:else}"a" is greater than "]"{/lte}{~n} +{@lte key="a" value="A"}"a" is less than or equal to "A"{:else}"a" is greater than "A"{/lte}{~n} +{@lte key="a" value="}"}"a" is less than or equal to "}"{:else}"a" is greater than "}"{/lte}{~n} +{! +Commented out because unicode breaks nom +{@lte key="☃" value="☄"}"☃" is less than or equal to "☄"{:else}"☃" is greater than "☄"{/lte}{~n} +!} +{@lte key=true_value value=false_value}true is less than or equal to false{:else}true is greater than false{/lte}{~n} +{@lte key=array_lower value=array_higher}[3,5,7] is less than or equal to [8,9]{:else}[3,5,7] is greater than [8,9]{/lte}{~n} +{@lte key=array_higher value=array_lower}[8,9] is less than or equal to [3,5,7]{:else}[8,9] is greater than [3,5,7]{/lte}{~n} +{! non-scalar and copied value tests !} +{@lte key=array_lower value=array_lower}array_lower is less than or equal to array_lower{:else}array_lower is greater than array_lower{/lte}{~n} +{@lte key=array_lower value=copy_array_lower}array_lower is less than or equal to copy_array_lower{:else}array_lower is greater than copy_array_lower{/lte}{~n} +{@lte key=some_obj value=some_obj}some_obj is less than or equal to some_obj{:else}some_obj is greater than some_obj{/lte}{~n} +{@lte key=some_obj value=copy_some_obj}some_obj is less than or equal to copy_some_obj{:else}some_obj is greater than copy_some_obj{/lte}{~n} +{@lte key=some_obj value=other_obj}some_obj is less than or equal to other_obj{:else}some_obj is greater than other_obj{/lte}{~n} +{@lte key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is less than or equal to array_of_some_obj{:else}array_of_some_obj is greater than array_of_some_obj{/lte}{~n} +{@lte key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is less than or equal to copy_array_of_some_obj{:else}array_of_some_obj is greater than copy_array_of_some_obj{/lte}{~n} +{@lte key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is less than or equal to array_of_other_obj{:else}array_of_some_obj is greater than array_of_other_obj{/lte}{~n} +{@lte key=array_higher value=array_higher_longer}array_higher is less than or equal to array_higher_longer{:else}array_higher is greater than array_higher_longer{/lte}{~n} +{@lte key=array_lower value=array_lower_with_array}array_lower is less than or equal to array_lower_with_array{:else}array_lower is greater than array_lower_with_array{/lte}{~n} +{@lte key=array_lower_with_array value=array_lower}array_lower_with_array is less than or equal to array_lower{:else}array_lower_with_array is greater than array_lower{/lte}{~n} diff --git a/js/test_cases/helpers_ne/input1.json b/js/test_cases/helpers_ne/input1.json new file mode 100644 index 0000000..bfbbd94 --- /dev/null +++ b/js/test_cases/helpers_ne/input1.json @@ -0,0 +1,60 @@ +{ + "str": "master", + "int": 7, + "alpha": 21, + "beta": "21", + "null": null, + "true_value": true, + "false_value": false, + "array_lower": [ + 3, + 5, + 7 + ], + "copy_array_lower": [ + 3, + 5, + 7 + ], + "array_higher": [ + 8, + 9 + ], + "some_obj": { + "name": "cat" + }, + "copy_some_obj": { + "name": "cat" + }, + "other_obj": { + "name": "dog" + }, + "array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "copy_array_of_some_obj": [ + { + "name": "cat" + }, + { + "name": "cat" + } + ], + "array_of_other_obj": [ + { + "name": "dog" + }, + { + "name": "dog" + } + ], + "array_of_strings": [ + "cat", + "dog" + ] +} diff --git a/js/test_cases/helpers_ne/main.dust b/js/test_cases/helpers_ne/main.dust new file mode 100644 index 0000000..d31e4b7 --- /dev/null +++ b/js/test_cases/helpers_ne/main.dust @@ -0,0 +1,28 @@ +Testing helpers:{~n} +str is {str}{~n} +int is {int}{~n} +alpha is {alpha}{~n} +beta is {beta}{~n} +{@ne key=str value="master"}str does not equal "master"{:else}str is equal to "master"{/ne}{~n} +{@ne key=str value="7"}str does not equal "7"{:else}str is equal to "7"{/ne}{~n} +{@ne key=int value="7"}int does not equal "7"{:else}int is equal to "7"{/ne}{~n} +{@ne key=int value=7}int does not equal 7{:else}int is equal to 7{/ne}{~n} +{@ne key=alpha value=beta}alpha does not equal beta{:else}alpha is equal to beta{/ne}{~n} +{@ne value=beta}missing key is true{:else}missing key is false{/ne}{~n} +{@ne value=gamma}missing key and non-existent value is true{:else}missing key and non-existent value is false{/ne}{~n} +{@ne key=alpha}missing value is true{:else}missing value is false{/ne}{~n} +{@ne key=gamma}missing value and non-existent key is true{:else}missing value and non-existent key is false{/ne}{~n} +{@ne key="master" value="master"}"master" does not equal "master"{:else}"master" is equal to "master"{/ne}{~n} +{@ne key=null}null does not equal a missing value{:else}null equals a missing value{/ne}{~n} +{@ne key=null value=gamma}null does not equal non-existent value{:else}null equals a non-existent value{/ne}{~n} +{@ne}no parameters is true{:else}no parameters is false{/ne}{~n} +{@ne key=array_lower value=array_higher}[3,5,7] does not equal [8,9]{:else}[3,5,7] is equal to [8,9]{/ne}{~n} +{! non-scalar and copied value tests !} +{@ne key=array_lower value=array_lower}array_lower does not equal array_lower{:else}array_lower is equals to array_lower{/ne}{~n} +{@ne key=array_lower value=copy_array_lower}array_lower does not equal copy_array_lower{:else}array_lower is equals to copy_array_lower{/ne}{~n} +{@ne key=some_obj value=some_obj}some_obj does not equal some_obj{:else}some_obj is equals to some_obj{/ne}{~n} +{@ne key=some_obj value=copy_some_obj}some_obj does not equal copy_some_obj{:else}some_obj is equals to copy_some_obj{/ne}{~n} +{@ne key=some_obj value=other_obj}some_obj does not equal other_obj{:else}some_obj is equals to other_obj{/ne}{~n} +{@ne key=array_of_some_obj value=array_of_some_obj}array_of_some_obj does not equal array_of_some_obj{:else}array_of_some_obj is equals to array_of_some_obj{/ne}{~n} +{@ne key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj does not equal copy_array_of_some_obj{:else}array_of_some_obj is equals to copy_array_of_some_obj{/ne}{~n} +{@ne key=array_of_some_obj value=array_of_other_obj}array_of_some_obj does not equal array_of_other_obj{:else}array_of_some_obj is equals to array_of_other_obj{/ne}{~n} diff --git a/src/bin.rs b/src/bin.rs index 12e1bd3..4274501 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,5 +1,6 @@ extern crate nom; +use crate::renderer::CompareContextElement; use parser::Filter; use renderer::compile_template; use renderer::CompiledTemplate; @@ -10,6 +11,7 @@ use renderer::RenderError; use renderer::Renderable; use renderer::WalkError; use renderer::Walkable; +use std::cmp::Ordering; use std::env; use std::fs; use std::io::{self, Read}; @@ -137,3 +139,120 @@ impl Loopable for serde_json::Value { } } } + +impl CompareContextElement for serde_json::Value { + fn equals(&self, other: &dyn ContextElement) -> bool { + // Handle other serde_json::Value + match other.to_any().downcast_ref::() { + None => (), + Some(other_json_value) => match (self, other_json_value) { + // Non-scalar values not caught in the renderer by the + // identical-path shortcut are always not equal. + (serde_json::Value::Array(_), _) + | (_, serde_json::Value::Array(_)) + | (serde_json::Value::Object(_), _) + | (_, serde_json::Value::Object(_)) => return false, + _ => return self == other_json_value, + }, + } + // Handle string literals + match other.to_any().downcast_ref::() { + None => (), + Some(other_string) => return self.as_str().map_or(false, |s| s == other_string), + } + // Handle numeric literals + match other.to_any().downcast_ref::() { + None => (), + Some(other_num) => return self.as_u64().map_or(false, |n| n == *other_num), + } + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // Handle other serde_json::Value + match other.to_any().downcast_ref::() { + None => (), + Some(other_json_value) => { + return match (self, other_json_value) { + ( + serde_json::Value::Bool(self_boolean), + serde_json::Value::Bool(other_boolean), + ) => self_boolean.partial_cmp(other_boolean), + ( + serde_json::Value::Number(self_number), + serde_json::Value::Number(other_number), + ) => match ( + self_number.as_f64(), + other_number.as_f64(), + self_number.as_u64(), + other_number.as_u64(), + self_number.as_i64(), + other_number.as_i64(), + ) { + (_, _, _, _, Some(self_int), Some(other_int)) => { + self_int.partial_cmp(&other_int) + } + (_, _, Some(self_uint), Some(other_uint), _, _) => { + self_uint.partial_cmp(&other_uint) + } + (_, _, Some(_self_uint), _, _, Some(_other_int)) => { + // If the previous matches did not catch + // it, then other must be negative and + // self must be larger than can be + // represented with an i64, therefore self + // is greater than other. + Some(Ordering::Greater) + } + (_, _, _, Some(_other_uint), Some(_self_int), _) => { + // If the previous matches did not catch + // it, then self must be negative and + // other must be larger than can be + // represented with an i64, therefore + // other is greater than self. + Some(Ordering::Less) + } + (Some(self_float), Some(other_float), _, _, _, _) => { + self_float.partial_cmp(&other_float) + } + _ => panic!("This should be impossible since u64 and i64 can both be converted to floats"), + }, + ( + serde_json::Value::String(self_string), + serde_json::Value::String(other_string), + ) => self_string.partial_cmp(other_string), + (serde_json::Value::Array(self_array), serde_json::Value::Array(other_array)) => convert_vec_to_context_element(self_array).partial_cmp(&convert_vec_to_context_element(other_array)), + _ => None, + }; + } + } + // Handle string literals + match other.to_any().downcast_ref::() { + None => (), + Some(other_string) => { + return self + .as_str() + .map_or(None, |s| s.partial_cmp(other_string.as_str())) + } + } + // Handle numeric literals + match other.to_any().downcast_ref::() { + None => (), + Some(other_num) => return self.as_u64().map_or(None, |n| n.partial_cmp(other_num)), + } + None + } +} + +/// Create a new vec by of references to the serde_json::Values as +/// ContextElement trait objects so we can use its implementation of +/// PartialOrd. +/// +/// You cannot implement a trait you do not define for a type you do +/// not define, so I cannot implement PartialOrd for +/// serde_json::value. Instead, I just re-use the PartialOrd +/// implementation for ContextElement which unfortunately has extra +/// overhead of downcasting. This would be a good spot for +/// optimization. +fn convert_vec_to_context_element(array: &Vec) -> Vec<&dyn ContextElement> { + array.iter().map(|v| v as _).collect() +} diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 768ec88..a742fa3 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -2,11 +2,11 @@ use nom::branch::alt; use nom::bytes::complete::escaped_transform; use nom::bytes::complete::is_a; use nom::bytes::complete::is_not; -use nom::bytes::complete::{tag, take_until, take_until_parser_matches}; +use nom::bytes::complete::{tag, take_until, take_until_parser_matches, take_while}; use nom::character::complete::line_ending; use nom::character::complete::multispace0; use nom::character::complete::one_of; -use nom::character::complete::{space0, space1}; +use nom::character::complete::{digit1, space0, space1}; use nom::combinator::all_consuming; use nom::combinator::map; use nom::combinator::opt; @@ -38,8 +38,8 @@ pub enum DustTag<'a> { DTHelperNotEquals(ParameterizedBlock<'a>), DTHelperGreaterThan(ParameterizedBlock<'a>), DTHelperLessThan(ParameterizedBlock<'a>), - DTHelperGreaterThenOrEquals(ParameterizedBlock<'a>), - DTHelperLessThenOrEquals(ParameterizedBlock<'a>), + DTHelperGreaterThanOrEquals(ParameterizedBlock<'a>), + DTHelperLessThanOrEquals(ParameterizedBlock<'a>), } #[derive(Clone, Debug, PartialEq)] @@ -106,10 +106,10 @@ pub struct NamedBlock<'a> { #[derive(Clone, Debug, PartialEq)] pub struct ParameterizedBlock<'a> { - name: &'a str, - params: Vec>, - contents: Option>, - else_contents: Option>, + pub name: &'a str, + pub params: Vec>, + pub contents: Option>, + pub else_contents: Option>, } #[derive(Clone, Debug, PartialEq)] @@ -122,6 +122,7 @@ pub struct Partial<'a> { pub enum RValue<'a> { RVPath(Path<'a>), RVString(String), + RVPositiveInteger(u64), } #[derive(Clone, Debug, PartialEq)] @@ -161,8 +162,8 @@ fn dust_tag(i: &str) -> IResult<&str, DustTag> { named_block("{+", DustTag::DTBlock), named_block("{<", DustTag::DTInlinePartial), partial("{>", DustTag::DTPartial), - parameterized_block("{@", "gte", DustTag::DTHelperGreaterThenOrEquals), - parameterized_block("{@", "lte", DustTag::DTHelperLessThenOrEquals), + parameterized_block("{@", "gte", DustTag::DTHelperGreaterThanOrEquals), + parameterized_block("{@", "lte", DustTag::DTHelperLessThanOrEquals), parameterized_block("{@", "eq", DustTag::DTHelperEquals), parameterized_block("{@", "ne", DustTag::DTHelperNotEquals), parameterized_block("{@", "gt", DustTag::DTHelperGreaterThan), @@ -210,11 +211,23 @@ fn path(i: &str) -> IResult<&str, Path> { ))(i) } +/// Just digits, no signs or decimals +fn postitive_integer_literal(i: &str) -> IResult<&str, u64> { + map( + verify( + map(digit1, |number_string: &str| number_string.parse::()), + |parse_result| parse_result.is_ok(), + ), + |parsed_number| parsed_number.unwrap(), + )(i) +} + /// Either a literal or a path to a value fn rvalue(i: &str) -> IResult<&str, RValue> { alt(( map(path, RValue::RVPath), map(quoted_string, RValue::RVString), + map(postitive_integer_literal, RValue::RVPositiveInteger), ))(i) } @@ -929,6 +942,29 @@ mod tests { ); } + #[test] + fn test_literals() { + assert_eq!( + dust_tag(r#"{>foo a="foo" b=179/}"#), + Ok(( + "", + DustTag::DTPartial(Partial { + name: "foo".to_owned(), + params: vec![ + KVPair { + key: "a", + value: RValue::RVString("foo".to_owned()) + }, + KVPair { + key: "b", + value: RValue::RVPositiveInteger(179) + } + ] + }) + )) + ); + } + #[test] fn test_helper() { assert_eq!( diff --git a/src/renderer/context_element.rs b/src/renderer/context_element.rs index dee6680..bb43cf8 100644 --- a/src/renderer/context_element.rs +++ b/src/renderer/context_element.rs @@ -1,9 +1,13 @@ use crate::parser::Filter; use crate::renderer::errors::RenderError; use crate::renderer::errors::WalkError; -use std::fmt::Debug; +use std::any::Any; +use std::{cmp::Ordering, fmt::Debug}; -pub trait ContextElement: Debug + Walkable + Renderable + Loopable {} +pub trait ContextElement: + Debug + Walkable + Renderable + Loopable + CloneIntoBoxedContextElement + CompareContextElement +{ +} pub trait Walkable { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError>; @@ -25,3 +29,41 @@ pub trait Loopable { /// for each element of the array. fn get_loop_elements(&self) -> Vec<&dyn ContextElement>; } + +pub trait CastToAny { + fn to_any(&self) -> &dyn Any; +} + +pub trait CompareContextElement: CastToAny { + fn equals(&self, other: &dyn ContextElement) -> bool; + + fn partial_compare(&self, other: &dyn ContextElement) -> Option; +} + +pub trait CloneIntoBoxedContextElement { + fn clone_to_box(&self) -> Box; +} + +impl CloneIntoBoxedContextElement for C { + fn clone_to_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl CastToAny for C { + fn to_any(&self) -> &dyn Any { + self + } +} + +impl<'a, 'b> PartialEq<&'b dyn ContextElement> for &'a dyn ContextElement { + fn eq(&self, other: &&'b dyn ContextElement) -> bool { + self.equals(*other) + } +} + +impl<'a, 'b> PartialOrd<&'b dyn ContextElement> for &'a dyn ContextElement { + fn partial_cmp(&self, other: &&'b dyn ContextElement) -> Option { + self.partial_compare(*other) + } +} diff --git a/src/renderer/errors.rs b/src/renderer/errors.rs index 5a38068..7dac064 100644 --- a/src/renderer/errors.rs +++ b/src/renderer/errors.rs @@ -9,6 +9,7 @@ pub enum RenderError { TemplateNotFound(String), } +#[derive(PartialEq)] pub enum WalkError { CantWalk, } diff --git a/src/renderer/inline_partial_tree.rs b/src/renderer/inline_partial_tree.rs index c58a5bb..ff7220b 100644 --- a/src/renderer/inline_partial_tree.rs +++ b/src/renderer/inline_partial_tree.rs @@ -99,6 +99,65 @@ fn extract_inline_partials_from_tag<'a, 'b>( blocks.insert(&named_block.name, &named_block.contents); } DustTag::DTBlock(..) => (), - _ => (), // TODO: Implement the rest + DustTag::DTHelperEquals(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTHelperNotEquals(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTHelperGreaterThan(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTHelperLessThan(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } + DustTag::DTHelperLessThanOrEquals(parameterized_block) => { + match ¶meterized_block.contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + match ¶meterized_block.else_contents { + None => (), + Some(body) => extract_inline_partials_from_body(blocks, &body), + }; + } } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 70f423b..45dbdfd 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -7,6 +7,8 @@ mod parameters_context; mod renderer; mod walking; +pub use context_element::CloneIntoBoxedContextElement; +pub use context_element::CompareContextElement; pub use context_element::ContextElement; pub use context_element::Loopable; pub use context_element::Renderable; diff --git a/src/renderer/parameters_context.rs b/src/renderer/parameters_context.rs index c2aa6d0..5812ce3 100644 --- a/src/renderer/parameters_context.rs +++ b/src/renderer/parameters_context.rs @@ -1,39 +1,75 @@ use crate::parser::KVPair; use crate::parser::{Filter, RValue}; +use crate::renderer::context_element::CompareContextElement; use crate::renderer::context_element::ContextElement; -use crate::renderer::walking::walk_path; +use crate::renderer::walking::owned_walk_path; use crate::renderer::Loopable; use crate::renderer::RenderError; use crate::renderer::Renderable; use crate::renderer::WalkError; use crate::renderer::Walkable; -use std::collections::HashMap; +use std::{cmp::Ordering, collections::HashMap}; -#[derive(Clone, Debug)] -pub struct ParametersContext<'a> { - params: HashMap<&'a str, &'a RValue<'a>>, - breadcrumbs: &'a Vec<&'a dyn ContextElement>, +/// Copy the data from an RValue to an Owned struct +/// +/// In order to get comparisons to work for our `ContextElement` trait +/// objects, we need to be able to use `std::any::Any`. Unfortunately, +/// `Any` requires that the structs do not have a lifetime (so they +/// will have a `'static` lifetime. This means that we cannot have a +/// `<'a>` appended to the struct type, so the struct cannot contain +/// any borrows. Rather than impose the copy cost in the parser, we +/// are imposing the cost of copying the data in the renderer because +/// the parser has no reason to not be able to reference data from the +/// input string. +#[derive(Clone, Debug, PartialEq)] +pub enum OwnedRValue { + RVPath(OwnedPath), + RVString(String), + RVPositiveInteger(u64), } -impl<'a> ParametersContext<'a> { - pub fn new( - breadcrumbs: &'a Vec<&'a dyn ContextElement>, - params: &'a Vec>, - ) -> ParametersContext<'a> { - let param_map = params - .iter() - .map(|pair: &KVPair<'a>| (pair.key, &pair.value)) - .collect(); - ParametersContext { - params: param_map, - breadcrumbs: breadcrumbs, +#[derive(Clone, Debug, PartialEq)] +pub struct OwnedPath { + pub keys: Vec, +} + +impl From<&RValue<'_>> for OwnedRValue { + fn from(original: &RValue<'_>) -> Self { + match original { + RValue::RVString(text) => OwnedRValue::RVString(text.to_owned()), + RValue::RVPath(path) => OwnedRValue::RVPath(OwnedPath { + keys: path.keys.iter().map(|k| k.to_string()).collect(), + }), + RValue::RVPositiveInteger(num) => OwnedRValue::RVPositiveInteger(*num), } } } -impl<'a> ContextElement for ParametersContext<'a> {} +#[derive(Debug)] +pub struct ParametersContext { + params: HashMap, + breadcrumbs: Vec>, +} -impl<'a> Renderable for ParametersContext<'a> { +impl ParametersContext { + pub fn new(breadcrumbs: &Vec<&dyn ContextElement>, params: &Vec) -> ParametersContext { + let owned_params: HashMap = params + .iter() + .map(|kvpair| (kvpair.key.to_string(), OwnedRValue::from(&kvpair.value))) + .collect(); + let owned_breadcrumbs: Vec> = + breadcrumbs.iter().map(|ce| ce.clone_to_box()).collect(); + + ParametersContext { + params: owned_params, + breadcrumbs: owned_breadcrumbs, + } + } +} + +impl ContextElement for ParametersContext {} + +impl Renderable for ParametersContext { fn render(&self, _filters: &Vec) -> Result { // TODO: Would this even ever be called? Won't matter, but I'd // like to know. Since it is injected 1 above the current @@ -42,7 +78,7 @@ impl<'a> Renderable for ParametersContext<'a> { } } -impl<'a> Loopable for ParametersContext<'a> { +impl Loopable for ParametersContext { fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { // TODO: Would this even ever be called? Won't matter, but I'd // like to know. Since it is injected 1 above the current @@ -51,16 +87,48 @@ impl<'a> Loopable for ParametersContext<'a> { } } -impl<'a> Walkable for ParametersContext<'a> { +impl Walkable for ParametersContext { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { let rval = self.params.get(segment).ok_or(WalkError::CantWalk)?; match rval { - RValue::RVPath(path) => walk_path(self.breadcrumbs, &path.keys), - RValue::RVString(text) => Ok(text), + OwnedRValue::RVPath(path) => owned_walk_path(&self.breadcrumbs, &path.keys), + OwnedRValue::RVString(text) => Ok(text), + OwnedRValue::RVPositiveInteger(num) => Ok(num), } } } +impl Clone for ParametersContext { + fn clone(&self) -> Self { + let new_params: HashMap = self + .params + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let new_breadcrumbs: Vec> = self + .breadcrumbs + .iter() + .map(|bread| bread.clone_to_box()) + .collect(); + ParametersContext { + params: new_params, + breadcrumbs: new_breadcrumbs, + } + } +} + +impl CompareContextElement for ParametersContext { + fn equals(&self, other: &dyn ContextElement) -> bool { + // TODO: Does this ever happen? perhaps I should have a panic here. + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // TODO: Does this ever happen? perhaps I should have a panic here. + None + } +} + impl ContextElement for String {} impl Renderable for String { @@ -84,3 +152,81 @@ impl Walkable for String { Err(WalkError::CantWalk) } } + +impl CompareContextElement for String { + fn equals(&self, other: &dyn ContextElement) -> bool { + // If its a String then compare them directly, otherwise defer + // to the other type's implementation of CompareContextElement + // since the end user could add any type. + match other.to_any().downcast_ref::() { + None => other.equals(self), + Some(other_string) => self == other_string, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // If its a string then compare them directly, otherwise defer + // to the other type's implementation of CompareContextElement + // since the end user could add any type. + match other.to_any().downcast_ref::() { + None => match other.partial_compare(self) { + None => None, + Some(ord) => match ord { + Ordering::Equal => Some(Ordering::Equal), + Ordering::Greater => Some(Ordering::Less), + Ordering::Less => Some(Ordering::Greater), + }, + }, + Some(other_string) => self.partial_cmp(other_string), + } + } +} + +impl ContextElement for u64 {} + +impl Renderable for u64 { + fn render(&self, _filters: &Vec) -> Result { + Ok(self.to_string()) + } +} + +impl Loopable for u64 { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + vec![self] + } +} + +impl Walkable for u64 { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + Err(WalkError::CantWalk) + } +} + +impl CompareContextElement for u64 { + fn equals(&self, other: &dyn ContextElement) -> bool { + // If its a u64 then compare them directly, otherwise defer + // to the other type's implementation of CompareContextElement + // since the end user could add any type. + match other.to_any().downcast_ref::() { + None => other.equals(self), + Some(other_num) => self == other_num, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // If its a u64 then compare them directly, otherwise defer + // to the other type's implementation of CompareContextElement + // since the end user could add any type. + match other.to_any().downcast_ref::() { + None => match other.partial_compare(self) { + None => None, + Some(ord) => match ord { + Ordering::Equal => Some(Ordering::Equal), + Ordering::Greater => Some(Ordering::Less), + Ordering::Less => Some(Ordering::Greater), + }, + }, + Some(other_num) => self.partial_cmp(other_num), + } + } +} diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index e6b7bd4..f44022c 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -1,6 +1,8 @@ use crate::parser::template; use crate::parser::Body; use crate::parser::DustTag; +use crate::parser::KVPair; +use crate::parser::RValue; use crate::parser::Special; use crate::parser::Template; use crate::parser::TemplateElement; @@ -12,7 +14,7 @@ use crate::renderer::inline_partial_tree::extract_inline_partials; use crate::renderer::inline_partial_tree::InlinePartialTreeElement; use crate::renderer::parameters_context::ParametersContext; use crate::renderer::walking::walk_path; -use std::collections::HashMap; +use std::{cmp::Ordering, collections::HashMap}; #[derive(Clone, Debug)] pub struct CompiledTemplate<'a> { @@ -75,6 +77,18 @@ impl<'a> DustRenderer<'a> { self.render_body(&main_template.contents, breadcrumbs, &new_blocks) } + fn render_maybe_body( + &'a self, + body: &'a Option, + breadcrumbs: &Vec<&'a dyn ContextElement>, + blocks: &'a InlinePartialTreeElement<'a>, + ) -> Result { + match body { + None => Ok("".to_owned()), + Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?), + } + } + fn render_body( &'a self, body: &'a Body, @@ -128,16 +142,13 @@ impl<'a> DustRenderer<'a> { } DustTag::DTSection(container) => { let val = walk_path(breadcrumbs, &container.path.keys); - let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val); + let loop_elements: Vec<&dyn ContextElement> = Self::get_loop_elements(val); if loop_elements.is_empty() { // Oddly enough if the value is falsey (like // an empty array or null), Dust uses the // original context before walking the path as // the context for rendering the else block - return match &container.else_contents { - Some(body) => self.render_body(&body, breadcrumbs, blocks), - None => Ok("".to_owned()), - }; + return self.render_maybe_body(&container.else_contents, breadcrumbs, blocks); } else { match &container.contents { None => return Ok("".to_owned()), @@ -158,33 +169,21 @@ impl<'a> DustRenderer<'a> { } DustTag::DTExists(container) => { let val = walk_path(breadcrumbs, &container.path.keys); - let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val); - if loop_elements.is_empty() { - return match &container.else_contents { - Some(body) => self.render_body(&body, breadcrumbs, blocks), - None => Ok("".to_owned()), - }; + let loop_elements: Vec<&dyn ContextElement> = Self::get_loop_elements(val); + return if loop_elements.is_empty() { + self.render_maybe_body(&container.else_contents, breadcrumbs, blocks) } else { - return match &container.contents { - None => Ok("".to_owned()), - Some(body) => self.render_body(&body, breadcrumbs, blocks), - }; - } + self.render_maybe_body(&container.contents, breadcrumbs, blocks) + }; } DustTag::DTNotExists(container) => { let val = walk_path(breadcrumbs, &container.path.keys); - let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val); - if !loop_elements.is_empty() { - return match &container.else_contents { - Some(body) => self.render_body(&body, breadcrumbs, blocks), - None => Ok("".to_owned()), - }; + let loop_elements: Vec<&dyn ContextElement> = Self::get_loop_elements(val); + return if !loop_elements.is_empty() { + self.render_maybe_body(&container.else_contents, breadcrumbs, blocks) } else { - return match &container.contents { - None => Ok("".to_owned()), - Some(body) => self.render_body(&body, breadcrumbs, blocks), - }; - } + self.render_maybe_body(&container.contents, breadcrumbs, blocks) + }; } DustTag::DTPartial(partial) => { if partial.params.is_empty() { @@ -200,28 +199,238 @@ impl<'a> DustRenderer<'a> { return Ok(rendered_content); } } - DustTag::DTInlinePartial(named_block) => { + DustTag::DTInlinePartial(_named_block) => { // Inline partials are blank during rendering (they get injected into blocks) return Ok("".to_owned()); } DustTag::DTBlock(named_block) => { - match blocks.get_block(named_block.name) { - None => match &named_block.contents { - None => return Ok("".to_owned()), - Some(body) => { - let rendered_content = self.render_body(body, breadcrumbs, blocks)?; - return Ok(rendered_content); - } - }, - Some(interior) => match interior { - None => return Ok("".to_owned()), - Some(body) => { - let rendered_content = self.render_body(body, breadcrumbs, blocks)?; - return Ok(rendered_content); - } - }, + return match blocks.get_block(named_block.name) { + None => self.render_maybe_body(&named_block.contents, breadcrumbs, blocks), + Some(interior) => self.render_maybe_body(interior, breadcrumbs, blocks), }; } + DustTag::DTHelperEquals(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + // Special case: when comparing two RVPaths, if the + // path is equal then dust assumes the values are + // equal (otherwise, non-scalar values are + // automatically not equal) + if Self::are_paths_identical(¶m_map) { + return match ¶meterized_block.contents { + None => Ok("".to_owned()), + Some(body) => { + let rendered_content = self.render_body(body, breadcrumbs, blocks)?; + Ok(rendered_content) + } + }; + } + + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + if left_side == right_side { + return self.render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ); + } + } + DustTag::DTHelperNotEquals(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + // Special case: when comparing two RVPaths, if the + // path is equal then dust assumes the values are + // equal (otherwise, non-scalar values are + // automatically not equal) + if Self::are_paths_identical(¶m_map) { + return match ¶meterized_block.else_contents { + None => Ok("".to_owned()), + Some(body) => { + let rendered_content = self.render_body(body, breadcrumbs, blocks)?; + Ok(rendered_content) + } + }; + } + + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + if left_side != right_side { + return self.render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ); + } + } + DustTag::DTHelperGreaterThan(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped > right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ); + } + } + } + } + DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + return match left_side_unwrapped.partial_cmp(&right_side_unwrapped) { + Some(Ordering::Greater) | Some(Ordering::Equal) | None => self + .render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ), + Some(Ordering::Less) => self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ), + }; + } + } + } + DustTag::DTHelperLessThan(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped < right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ); + } + } + } + } + DustTag::DTHelperLessThanOrEquals(parameterized_block) => { + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res, + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + return match left_side_unwrapped.partial_cmp(&right_side_unwrapped) { + Some(Ordering::Less) | Some(Ordering::Equal) | None => self + .render_maybe_body( + ¶meterized_block.contents, + breadcrumbs, + blocks, + ), + Some(Ordering::Greater) => self.render_maybe_body( + ¶meterized_block.else_contents, + breadcrumbs, + blocks, + ), + }; + } + } + } _ => (), // TODO: Implement the rest } Ok("".to_owned()) @@ -232,7 +441,6 @@ impl<'a> DustRenderer<'a> { /// If the value is falsey, and therefore should render the else /// block, this will return an empty vector. fn get_loop_elements<'b>( - &'a self, walk_result: Result<&'b dyn ContextElement, WalkError>, ) -> Vec<&'b dyn ContextElement> { match walk_result { @@ -240,6 +448,41 @@ impl<'a> DustRenderer<'a> { Ok(walk_target) => walk_target.get_loop_elements(), } } + + fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool { + match (param_map.get("key"), param_map.get("value")) { + (None, _) => false, + (_, None) => false, + (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) { + (RValue::RVPath(key_path), RValue::RVPath(value_path)) => { + key_path.keys == value_path.keys + } + _ => false, + }, + } + } + + fn get_rval_map<'b>(params: &'b Vec>) -> HashMap<&'b str, &'b RValue<'b>> { + params + .iter() + .map(|pair: &KVPair<'b>| (pair.key, &pair.value)) + .collect() + } + + fn get_rval<'b>( + breadcrumbs: &Vec<&'b dyn ContextElement>, + param_map: &HashMap<&str, &'b RValue<'b>>, + key: &str, + ) -> Option> { + match param_map.get(key) { + None => None, + Some(rval) => match rval { + RValue::RVString(text) => Some(Ok(text)), + RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)), + RValue::RVPositiveInteger(num) => Some(Ok(num)), + }, + } + } } #[cfg(test)] @@ -249,88 +492,76 @@ mod tests { use crate::renderer::context_element::Loopable; use crate::renderer::context_element::Renderable; use crate::renderer::context_element::Walkable; + use crate::renderer::CompareContextElement; - impl ContextElement for u32 {} - impl ContextElement for &str {} - impl ContextElement for HashMap<&str, I> {} + impl ContextElement for HashMap {} - impl Renderable for u32 { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Ok(self.to_string()) - } - } - - impl Renderable for &str { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Ok(self.to_string()) - } - } - - impl Renderable for HashMap<&str, I> { + impl Renderable for HashMap { fn render(&self, _filters: &Vec) -> Result { // TODO: handle the filters Ok("[object Object]".to_owned()) } } - impl Walkable for HashMap<&str, I> { + impl Walkable for HashMap { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { let child = self.get(segment).ok_or(WalkError::CantWalk)?; Ok(child) } } - impl Walkable for &str { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - Err(WalkError::CantWalk) - } - } - - impl Walkable for u32 { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - Err(WalkError::CantWalk) - } - } - - impl Loopable for &str { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - if self.is_empty() { - Vec::new() - } else { - vec![self] - } - } - } - - impl Loopable for u32 { + impl Loopable for HashMap { fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { vec![self] } } - impl Loopable for HashMap<&str, I> { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - vec![self] + impl CompareContextElement for HashMap { + fn equals(&self, other: &dyn ContextElement) -> bool { + false } } #[test] fn test_walk_path() { - let context: HashMap<&str, &str> = - [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] - .iter() - .cloned() - .collect(); - let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)] - .iter() - .cloned() - .collect(); - let deep_context: HashMap<&str, HashMap<&str, &str>> = [ - ("cat", [("food", "meat")].iter().cloned().collect()), - ("dog", [("food", "meat")].iter().cloned().collect()), - ("tiger", [("food", "people")].iter().cloned().collect()), + let context: HashMap = [ + ("cat".to_string(), "kitty".to_string()), + ("dog".to_string(), "doggy".to_string()), + ("tiger".to_string(), "murderkitty".to_string()), + ] + .iter() + .cloned() + .collect(); + let number_context: HashMap = [ + ("cat".to_string(), 1), + ("dog".to_string(), 2), + ("tiger".to_string(), 3), + ] + .iter() + .cloned() + .collect(); + let deep_context: HashMap> = [ + ( + "cat".to_string(), + [("food".to_string(), "meat".to_string())] + .iter() + .cloned() + .collect(), + ), + ( + "dog".to_string(), + [("food".to_string(), "meat".to_string())] + .iter() + .cloned() + .collect(), + ), + ( + "tiger".to_string(), + [("food".to_string(), "people".to_string())] + .iter() + .cloned() + .collect(), + ), ] .iter() .cloned() diff --git a/src/renderer/walking.rs b/src/renderer/walking.rs index feadce3..cc91047 100644 --- a/src/renderer/walking.rs +++ b/src/renderer/walking.rs @@ -51,3 +51,23 @@ pub fn walk_path<'a>( } Err(WalkError::CantWalk) } + +pub fn owned_walk_path<'a>( + breadcrumbs: &'a Vec>, + path: &Vec, +) -> Result<&'a dyn ContextElement, WalkError> { + let path_reference: Vec<&str> = path.iter().map(|p| &p[..]).collect(); + for context in breadcrumbs.iter().rev() { + match walk_path_from_single_level(context.as_ref(), &path_reference) { + // If no walking was done at all, keep looping + WalkResult::NoWalk => {} + // If we partially walked then stop trying to find + // anything + WalkResult::PartialWalk => { + return Err(WalkError::CantWalk); + } + WalkResult::FullyWalked(new_context) => return Ok(new_context), + } + } + Err(WalkError::CantWalk) +}