Problem/Motivation
The Subrequests module provides replacement token functionality, so that data returned by a prior request can be used in a subsequent request.
For example, you may have one request to create a paragraph, and then a second to create a page that contains that paragraph. That second request requires the id and the revision id of the paragraph created in the first request. These are of types string and integer respectively.
Using jsonapi, this would require a subrequest of the following form:
{
"requestId": "page",
"waitFor": ["paragraph"],
"uri": "/jsonapi/node/article",
"action": "create",
"body": "{\"data\":{\"type\":\"node--article\",\"attributes\":{\"title\":\"Page created with json api and subrequests\"},\"relationships\":{\"field_sections\":{\"data\":[{\"type\":\"paragraph--wysiwyg_basic\",\"id\":\"{{paragraph.body@$.data.id}}\",\"meta\":{\"target_revision_id\":{{paragraph.body@$.data.attributes.drupal_internal__revision_id}}}}]}}}}",
"headers": {
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
"Authorization": "Basic ohhaithere=="
}
}
On replacement of the two tokens {{paragraph.body@$.data.id}} and {{paragraph.body@$.data.attributes.drupal_internal__revision_id}} this would become valid JSON.
However, before token replacement, this is not valid JSON because {{paragraph.body@$.data.attributes.drupal_internal__revision_id}} contains non-numeric characters and is not quoted.
JsonBlueprintDenormalizer::fillDefaults() does $raw_item['body'] = Json::decode($raw_item['body']);
. Because our non-quoted token isn't valid JSON, this returns null, and further down the stack an exception is thrown, and this subrequest fails.
Note in this example we can work around this by quoting the token and pretending it is a string and letting PHP magic convert it as appropriate. But this is a fudge and would fall down with boolean values. Ultimately introduction of tokens means subrequests aren't actually JSON, so we shouldn't assume they are.
Proposed resolution
Don't parse non-JSON as if it's JSON.
Remaining tasks
All the things.
User interface changes
None.
API changes
Hmmm.
Data model changes
None.
Release notes snippet
✄
Comment | File | Size | Author |
---|---|---|---|
#12 | 3048493--interdiff--11-12.txt | 974 bytes | e0ipso |
#12 | 3048493--add-bool-handling-patch--12.patch | 3.77 KB | e0ipso |
#11 | 3048493_11_add_bool_handling.patch | 2.82 KB | Eli-T |
#8 | 3048493_8.patch | 2.76 KB | Eli-T |
Comments
Comment #2
e0ipsoThanks for contributing!
This is true. It's far from ideal, but a workaround for booleans is "0" and "1".
I don't agree with this statement. I think that the token is a string. The problem is that the calculated replacement is always treated as a string, regardless of the actual type (do you agree?). The replacement may or may not need type casting, and that's the actual problem. Maybe something like
\"id\":\"{{paragraph.body@$.data.id:int}}\"
would do. Or maybe it's unnecessary and we can detect the type from the previous subresponse during replacement time.I'm leaning towards the second.
Comment #3
Eli-TThanks for the rapid response, and all your work on this module!
I think that's fine if it's explicitly stated that the token must always be wrapped in quotes and those quotes can actually be replaced; effectively this makes the quotes as much part of the token as the surrounding pairs of braces.
If possible that seems far more flexible and elegant than embedding type data into the subrequest.
Comment #4
e0ipsoGlad to see we are on the same page. Any chances you can give this a try?
Comment #5
Eli-TI'll take a look this afternoon :)
Comment #6
Eli-TWhen the token replacement is actually being performed in JsonPathReplacer::replaceTokenSubject($token, $value, $token_subject), $value is passed in as int if that's what the token is when retrieved from the source.
So at that point, before we do the preg_replace(), we could check if $value is *not* a string, and if so, first attempt to replace with $token wrapped in double quotes.
But it's entirely possible that a token representing a non-string is embedded within a string. EG
so we'd have to also try to preg_replace() without wrapping in quotes to find such occurrences.
At that point, we can't tell if the intention of
"arbitrary thing": "{{paragraph.body@$.data.attributes.drupal_internal__revision_id}}"
in the subrequest is to embed the integer in the string that happens to have nothing else in it, or whether it should be replaced as a JSON integer.
So I'm not sure approach 2 in #2 is actually possible 🙁
Comment #7
e0ipsoLuckily there are not many scalar types in JSON. From json.org
We can remove the quotes during the replace if:
number
,true
,false
ornull
That would solve the issue, right?
Comment #8
Eli-TWork in progress, with tests.
This works with everything except true/false, as they are not passed in to replaceTokenSubject() as PHP booleans.
We'd also need to change the message in the exception thrown in validateJsonPathReplacements() as we are not expecting a list of strings anymore.
Comment #9
Eli-T(also need to remove the type hint for replaceTokenSubject() $value parameter)
Comment #10
Eli-TAlso JsonPathReplacerTest::testReplaceBatchTypes() has a var_dump() that needs removing.
Comment #11
Eli-TFixed bool handling and removed var_dump() mentioned in #10.
Probably a good time for a review :)
Comment #12
e0ipsoWell, all versions prior to 5 of the schema validator have vanished. I'm curious about what happened.
Comment #13
e0ipsoTests are passing locally. Merging.
Comment #15
Eli-TAwesome! I'll raise a follow up to address the change to the message in the exception thrown in validateJsonPathReplacements() as we are not expecting a list of strings anymore, as mentioned in #8. But tomorrow :)
Comment #16
e0ipsoFantastic! I wanted to get this committed soon. I agree, we can address that in a follow up.