Proposal - Amend Test Specification / Error Handling #20
Replies: 10 comments 43 replies
-
|
Something to think about before I undraft this: The following rule gives me pause, and we could decide to avoid these cases, { "*": ["h", 5] }This guy would emit Should errors be expected to halt / prevent logic execution? I would think so, and if so, I think that helps us set guidelines for what error boundaries we want to test for. |
Beta Was this translation helpful? Give feedback.
-
|
Some excerpts from another dicussion over JSON Logic error behaviors. From @gregsdennis
From @dslmeinte
From me
We need to decide if JSON Logic will support the concept of "Soft Errors" which doesn't halt execution and instead emit some value, or if errors will exclusively halt termination. I can see arguments both ways on numeric conversion. How do we handle It'd be a bit messy if we allowed |
Beta Was this translation helpful? Give feedback.
-
|
I have an idea I'll tinker with this weekend w/r/t "soft errors", and how to make them viable across implementations. I'm personally not really leaning heavily towards either approach. I believe deciding everything is a Hard Error would be much simpler from a specification perspective, but potentially reduces capability / efficiency from an AST perspective. -- Keep in mind that this is half-baked and I'm merely playing with the idea for soft-errors to see if there's any viability in this. Right now, the idea I'm contemplating is some sort of engine.addMethod('error', ([type]) => {
// Implementations could choose to substitute values that make sense
if (type === 'NaN') return Number.NaN
// literally returns an object;
// it could be best if this was some stable reference for easier comparison, rather than a new
// object each time.
return { error: type }
})And to check for this soft error, which is actually a data value, one could do: [
{
"description": "Checks if 0 / 0 produces NaN",
"rule": { "/": [0, 0] },
"result": { "error": "NaN" },
"data": null
},
{
"description": "Adding 1 to NaN",
"rule": { "+": [1, { "error": "NaN" }] },
"result": { "error": "NaN" },
"data": null
}
]In this case, there'd be an equivalence established in the test runner between Implementations / Architectures without a native NaN concept would have to pass the JSON Object around. There is a question of truthiness, in JS, On the surface, it seems like this could be alright, but what happens when I pass in a context with: {
"x": { "error": "NaN" }
}And call Could this ever lead to unintended behavior, or work in unintuitive ways? There are mental models where I could see this working out fine.. but also ones where this could cause problems. |
Beta Was this translation helpful? Give feedback.
-
|
@JTeeuwissen said:
(Funnily enough, I actually commited an experiment on that shortly before: json-logic/json-logic-engine@5d5c38d) |
Beta Was this translation helpful? Give feedback.
-
|
So I've been experimenting with the soft-error construct and... it's actually seemingly decent. It's OK. I think there's an argument for soft-error viability. What I've implemented:
The coalescing allows us to do something like { "try": [{ "+": [{ "val": "x" }, 1]}, 42] } // Falls back to 42 if val x cannot coerce to a number
|
Beta Was this translation helpful? Give feedback.
-
|
Before it is snowed under in a comment section, I propose a somewhat different approach: For the testing data Where each rule either returns a result, or an error, to be delivered in a language fitting manner (exceptions, monads, etc.) The difference between data and error implies that they are handled differently from each other, error not simply being data in a special format (which would not play nice with operators using objects as input). |
Beta Was this translation helpful? Give feedback.
-
|
At this phase, it appears that sentiment is leaning towards soft errors. I'm going to leave this open for a bit longer to offer folks time to vote on each. (You can vote independently on each solution, and it's okay to be comfortable with both!) |
Beta Was this translation helpful? Give feedback.
-
|
At this point, I'm going to revise the root proposal in favor of soft errors, but if there ends up being more support for hard errors, I can adjust accordingly. |
Beta Was this translation helpful? Give feedback.
-
|
I'm voting +1 on Option A, +0.5 on Option B. |
Beta Was this translation helpful? Give feedback.
-
Current Votes:
Needs one more approval for "A" to go through, @codetiger are you on board with A? |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Background
We currently have a test specification format here: https://github.com/json-logic/.github/blob/main/TEST_FORMAT.md
This is great for positive testing; but we might want to amend our specification to make things simple for negative testing, as well.
Positive Testing - Checking that functionality behaves as expected.
Negative Testing - Checking that functionality fails as expected.
Proposal
I'd like to amend the test format to essentially be:
Essentially, every rule has:
But the rule must have either:
or
But not both.
This would potentially allow us to define a test case like:
[ { "description": "Panic with an error emitted from an operator", "rule": { "/": [1, 0] }, "error": { "type": "NaN" }, "data": null } ]I'm going to do something dumb, and propose two options. Please vote on each independently.
**Option A (Hard Errors / Exceptions)**
With this proposal, we will define that errors should bubble up the AST and halt execution.
Implementations will be responsible for deciding how they want to emit and represent hard errors.
With this proposal, we will introduce a few operators:
throwtry{ "type": "type" }Test Cases
throwExpand
[ { "description": "Throws hello as an error", "rule": { "throw": "hello" }, "data": null, "error": { "type": "hello" } }, { "description": "Throwing NaN produces an error object or measured equivalent (equivalence class test)", "rule": { "throw": "NaN" }, "data": null, "error": { "type": "NaN" } }, { "description": "Can throw an error object", "rule": { "throw": { "val": "x" } }, "data": { "x": { "type": "Some error" }}, "error": { "type": "Some error" } } ]tryExpand
[ "# Collection of Tests for Try", { "description": "Coalesce an error", "rule": { "try": [{ "throw": "Some error" }, 1] }, "result": 1, "data": null }, { "description": "Coalesce an error emitted from an operator", "rule": { "try": [{ "/": [0, 0]}, 1] }, "result": 1, "data": { "hello": "world" } }, { "description": "Try is variadic", "rule": { "try": [{ "throw": "Some error" }, { "/": [0, 0] }, 2] }, "result": 2, "data": null }, { "description": "Panics if none of the values are valid", "rule": { "try": [{ "throw": "Some error" }, { "throw": "Some other error" }] }, "error": { "type": "Some other error" }, "data": null }, { "description": "Panics if none of the values are valid (2)", "rule": { "try": [{ "throw": "Some error" }, { "/": [0, 0] }] }, "error": { "type": "NaN" }, "data": null }, { "description": "Panics if none of the values are valid (3)", "rule": { "try": [{ "/": [0, 0] }, { "/": [1, 0] }, { "/": [2, 0] }] }, "error": { "type": "NaN" }, "data": null }, { "description": "Panics when the only argument is an error", "rule": { "try": { "throw": "Some error" } }, "error": { "type": "Some error" }, "data": null }, { "description": "Panic with an error emitted from an operator", "rule": { "try": [{ "/": [1, 0] }] }, "error": { "type": "NaN" }, "data": null }, { "description": "Panic within an iterator", "rule": { "map": [[1, 2, 3], { "try": [{ "/": [0,0] }] }] }, "error": { "type": "NaN" }, "data": null }, { "description": "Panic based on an error emitted from an if", "rule": { "try": [{ "if": [{"val": ["user", "admin"]}, true, { "throw": "Not an admin" }] }] }, "data": { "user": { "admin": false } }, "error": { "type": "Not an admin" } }, { "description": "Try can work further up the AST with Exceptions", "rule": { "try": [{ "if": [ true, { "map": [[1,2,3], {"/": [0, 0] }]}, null ] }, 10] }, "result": 10, "data": null }, { "description": "The context switches for the try coalescing to the previous error", "rule": { "try": [ { "throw": "Some error" }, { "val": "type" } ] }, "result": "Some error", "data": null }, { "description": "The context switches for the try coalescing to the previous error (2)", "rule": { "try": [ { "if": [true, { "throw": "Some error" }, null] }, { "val": "type" } ] }, "result": "Some error", "data": null }, { "description": "The context switches for the try coalescing to the previous error (3)", "rule": { "try": [ { "throw": "A" }, { "throw": "B" }, { "val": "type" } ] }, "result": "B", "data": null }, { "description": "Error can pull from an error object", "rule": { "try": [{ "throw": { "val": "x" } }, { "val": "type" }] }, "data": { "x": { "type": "Some error" }}, "result": "Some error" }, { "description": "Try can work further up the AST with Exceptions, and return the error", "rule": { "try": [{ "if": [ true, { "map": [[1,2,3], {"/": [0, 0] }]}, null ] }, { "val": "type" }] }, "result": "NaN", "data": null }, { "description": "Handles NaN Explicitly", "rule": { "try": [ { "if": [{ "/": [1, { "val": "x" }] }, { "throw": "Some error" }, null] }, { "if": [{ "===": [{ "val": "type" }, "NaN"]}, "Handled", { "throw": { "val": [] } }] } ] }, "result": "Handled", "data": { "x": 0 } }, { "description": "Did not NaN, so it errored", "rule": { "try": [ { "if": [{ "/": [1, { "val": "x" }] }, { "throw": "Some error" }, null] }, { "if": [{ "===": [{ "val": "type" }, "NaN"]}, "Handled", { "throw": { "val": [] } }] } ] }, "error": { "type": "Some error" }, "data": { "x": 1 } } ]**Option B (Soft Errors)** -- (Withdrawn)
This option has been withdrawn.
With this proposal, we will recognize two forms of errors:
error. These errors can be handled in the AST. (NaNis an example of a Soft Error)For soft errors, implementations may choose to create equivalence classes, to define a language construct as being equal to a soft error. (
NaNcan be made equivalent to{ "error": "NaN" })Implementations will be responsible for deciding how they want to emit and represent hard errors.
With this proposal, we will introduce a few operators:
errorImplementations may choose to substitute the return value for an equivalent language construct, as long as it's handled the exact same way.
try??andorwith short-circuit behavior. It will coalesce to the first non-error argument, or it will panic.**(Do we want this to return the last argument instead? If so, I'll reintroduce
panic)Test Cases
errorExpand
[ { "description": "Creates an error object", "rule": { "error": "hello" }, "data": null, "result": { "error": "hello" } }, { "description": "NaN creates an error object or measured equivalent (equivalence class test)", "rule": { "error": "NaN" }, "data": null, "result": { "error": "NaN" } } ]tryExpand
[ "# Collection of Tests for Try", { "description": "Coalesce an error", "rule": { "try": [{ "error": "Some error" }, 1] }, "result": 1, "data": null }, { "description": "Coalesce an error emitted from an operator", "rule": { "try": [{ "/": [0, 0]}, 1] }, "result": 1, "data": { "hello": "world" } }, { "description": "Coalesce an error pulled from context; errors are just data.", "rule": { "try": [{ "val": "x" }, 1]}, "data": { "x": { "error": "Some error" }}, "result": 1 }, { "description": "Panics if none of the values are valid", "rule": { "try": [{ "error": "Some error" }, { "error": "Some other error" }] }, "error": true, "data": null }, { "description": "Panics if none of the values are valid (2)", "rule": { "try": [{ "error": "Some error" }, { "/": [0, 0] }] }, "error": true, "data": null }, { "description": "Can promote a soft error to a hard error", "rule": { "try": { "error": "Some error" } }, "error": true, "data": null }, { "description": "Panic with an error emitted from an operator", "rule": { "try": [{ "/": [1, 0] }] }, "error": true, "data": null }, { "description": "Panic with an error pulled from context", "rule": { "try": [{ "val": "x" }] }, "error": true, "data": { "x": { "error": "Some error" } } }, { "description": "Panic within an iterator", "rule": { "map": [[1, 2, 3], { "try": [{ "/": [0,0] }] }] }, "error": true, "data": null }, { "description": "Panic based on an error emitted from an if", "rule": { "try": [{ "if": [{"val": ["user", "admin"]}, true, { "error": "Not an admin" }] }] }, "data": { "user": { "admin": false } }, "error": true } ]Related Materials
For Option A:
An example of how the numeric tests would work. They cause an error.
For Option B:
Additional Decisions (Both Proposals)
NaNis an Error. In Option A, this is a Hard Error. In Option B, this is a Soft Error.NaNBeta Was this translation helpful? Give feedback.
All reactions