Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add policy hooks to std.json to support custom extensions #6059

Closed
wants to merge 1 commit into from

Conversation

marler8997
Copy link
Contributor

@marler8997 marler8997 commented Jan 23, 2018

This is an exploration on enhancing std.json. The idea is to add support to preserve order of object members. An example of where this is useful is if you'd like to parse some JSON and then later write it somewhere, but you'd like to maintain the original order of the object members.

Note that the techniques used in this PR could also be reused for adding other optional features. It allows the application to pass in a Policy structure that can enable/disable features. If you wanted to add another feature, say, "enableComments", this could be added as a Policy setting as well. This technique could also be ported to the upcoming std.data.json.

Preserving field order is beneficial for any kind of tool that modifies JSON files. You could imagine tools that modify dub.json files, DMD JSON output files, sublime/vscode config files. If a tool like this couldn't guarantee preserving the order of object fields, then this would make the tools much less usable.

NOTE: This PR is not fully documented. The work to fully document is pending approval that this is a desirable enhancement.

@dlang-bot
Copy link
Contributor

Thanks for your pull request, @marler8997! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.

Some tips to help speed things up:

  • smaller, focused PRs are easier to review than big ones

  • try not to mix up refactoring or style changes with bug fixes or feature enhancements

  • provide helpful commit messages explaining the rationale behind each change

Bear in mind that large or tricky changes may require multiple rounds of review and revision.

Please see CONTRIBUTING.md for more information.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

std/json.d Outdated
foreach (ref member; members)
if (name == member.name)
return member.value;
assert(0, "JSON object does not contain member named: " ~ name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enforce for external / environmental errors

std/json.d Outdated
struct JSONValue
alias JSONValue = JSONValueTemplate!DefaultPolicy;

private struct JSONValueTemplate(alias Policy)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not private

std/json.d Outdated
ref auto opIndex(in char[] name) const
{
foreach (ref member; members)
if (name == member.name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing indent

std/json.d Outdated
return member.value;
assert(0, "JSON object does not contain member named: " ~ name);
}
ref auto opIndexAssign(Value value, string name)
Copy link
Contributor

@timotheecour timotheecour Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref auto => u probably wanna return *this ?
or how about using same signature as in std.json:
pure void opIndexAssign(ref Value value, string key);

std/json.d Outdated
alias Value = typeof(this);
static if (Policy.preserveMemberOrder)
{
struct JSONObject
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: this is basically an AA with ordered fields; it's re-usable; why not expose it as a generic:

struct AAOrdered(K,V){
...
}

@marler8997 marler8997 force-pushed the jsonOrder branch 2 times, most recently from 2fb8b15 to e6f1e64 Compare January 23, 2018 23:55
Copy link
Contributor

@timotheecour timotheecour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

std/json.d Outdated
enum preserveMemberOrder = true;
}

struct OrderedAA(K,V)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to std.array? (eg has assocArray etc)

Copy link
Contributor

@timotheecour timotheecour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Contributor

@timotheecour timotheecour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

std/json.d Outdated
}
private Member[] members;
size_t length() const { return members.length; }
inout(V)* opIn(in char[] name) inout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

char[] instead of K ? (do we need specialization for case K==string to allow char[] in that case?)
=> a unitest with orderedAA!(int, double) would've caught this bug

std/json.d Outdated
return &member.value;
return null;
}
ref auto opIndex(in char[] name) const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

std/json.d Outdated
return member.value;
assert(0, "OrderedAA does not contain member named: " ~ name);
}
void opIndexAssign(V value, K name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/name/key/ everywhere in this struct

std/json.d Outdated
foreach (ref member; members)
if (name == member.name)
return member.value;
assert(0, "OrderedAA does not contain member named: " ~ name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enforce!JSONException ?

std/json.d Outdated
{
foreach (name; names)
foreach (member; obj.members)
Copy link
Contributor

@timotheecour timotheecour Jan 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foreach (key, value; obj) {...}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in the DefaultPolicy case, names is just an array of strings, not an associative array.

std/json.d Outdated
() @trusted { store.object = aa; }();
}
}
else static if (Policy.preserveMemberOrder && is(T == JSONObject))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems restrictive compared to other policy which didn't only accept JSONObject but also things that converted to it via Key : string and Value(value)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. It seems a bit odd to take something that is unordered and then copy it to something that is ordered. Not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An application could always do the conversion themselves to. This is something that doesn't have to be implemented in the library (AFAICT).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it could be AAordered!(Key, Value) with Key and Value convertible to destination types

std/array.d Outdated
foreach (ref member; array)
if (key == member.key)
return member.value;
assert(0, "OrderedAA does not contain value with key: " ~ key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ~ key will fail with non-string types
  • unittest to catch that eg OrderedAA!(int, double)
  • not sure if assert is appropriate; it probably should be same type of error as corresponding error in an associative array

std/array.d Outdated
K key;
V value;
}
private KeyValue[] array;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that more efficient with Appender!KeyValue?

std/array.d Outdated

struct OrderedAA(K,V)
{
struct KeyValue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static struct? to avoid hidden __this pointer

@marler8997 marler8997 force-pushed the jsonOrder branch 2 times, most recently from f52621e to 3d33c71 Compare January 24, 2018 05:08
@JackStouffer
Copy link
Member

This seems like a misapplication of JSON. JSON objects are designed to be unordered; from the spec:

An object is an unordered set of name/value pairs.

My inclination is that if what you're doing with JSON requires you to keep the same order of keys, then there's something wrong with your design.

@quickfur
Copy link
Member

My main hesitation with this feature is that it doesn't conform to the JSON spec. It's one thing to make it possible for user code to do whatever it wants, which is why I gravitate towards a SAX-style / stream-based parser, because that enables user code to implement order-dependence without actually explicitly doing that in Phobos. But it's another thing for Phobos to explicitly support something that, strictly speaking, doesn't follow the JSON spec. I'm all for enabling the user, but I'm not a fan of vendor-specific extensions (which is basically what this PR is) that eventually leads to fragmentation.

And just to be clear, I'm not complaining merely because I like being pedantic about spec conformance; the underlying issue is that we're trying to impose order upon a structure that has been explicitly defined to be unordered. This, to me, is a code smell. In my experience, this style of coding often has or leads to fragility, unhandled corner cases, exceptional behaviours, and lots of little crufty things like that, that are symptomatic of an inherent impedance mismatch between what the code does and what the data is supposed to represent. I don't feel strongly enough about std.json to actively blockade this PR, the others can merge if they think it's a good idea, but I'll just say that I won't actively support this direction.

@marler8997
Copy link
Contributor Author

@quickfur thanks for explaining yourself, I see where you are coming from now. I'm thinking a better solution might be to provide a mechanism to allow the user to override the type used for JSONObject with the default being JSONValue[string]. If you're ok with this I'll go ahead and modify this PR to do that.

@marler8997
Copy link
Contributor Author

Pinging @quickfur and @JackStouffer

I've refactored the change. Now it allows you to override the default type for JSON objects, but does not provide any of these types. This allows an application to customize the parser to their needs without the parser providing any non-spec functionality out of the box. Note that I've included the OrderedAA as a unittest, but not as a publicly accessible structure. Please have a look and let us know if you are OK with this design.

@marler8997 marler8997 changed the title An attempt to enhance std.json to support preserving object member order Add policy to std.json to support custom extensions Jan 27, 2018
@marler8997 marler8997 changed the title Add policy to std.json to support custom extensions Add policy hooks to std.json to support custom extensions Jan 27, 2018
@marler8997 marler8997 force-pushed the jsonOrder branch 2 times, most recently from 2773aee to 185dcce Compare January 27, 2018 10:51
std/json.d Outdated
{
auto o = this.objectNoRef;
return *enforce!JSONException(k in o,
"Key not found: " ~ k);
static if (hasMember!(Policy, "ObjectType"))
Copy link
Contributor

@timotheecour timotheecour Jan 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY:

static if (hasMember!(Policy, "ObjectType"))
  auto ok=o.opIn(k);
else
  auto ok=k in o;
return *enforce!JSONException(ok, "Key not found: " ~ k);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just discovered that opIn_r was a thing...now that I know about it, this code only has the one case now :)

std/json.d Outdated
private struct DefaultPolicy
{
enum sortObjectMembers = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can simplify a lot the rest of the code by always requiring a template ObjectType to Policy:

private struct DefaultPolicy
 { 
   enum sortObjectMembers = true;
   alias ObjectType(Value)=Value[string];
 }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which code can be simplified?

std/json.d Outdated
@@ -1110,6 +1165,91 @@ if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T))
assert(json["key1"]["key2"].integer == 1);
}

version(unittest)
{
struct OrderedAA(Key, Value)
Copy link
Contributor

@timotheecour timotheecour Jan 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think others (not me...) were concerned about exposing json with ordered_fields, but I didn't see objections to exposing OrderedAA in std.array; as you currently wrote it by hiding OrderedAA inside a unittest block, this'll require someone who wants to use json with ordered_fields to reimplement OrderedAA below, which is not DRY and much worse than exposing json with ordered_fields.

In short: using json with ordered_fields should be as simple as:

import std.array:OrderedAA;
static struct OrderedAAPolicy
{
alias ObjectType = OrderedAA;
enum sortObjectMembers = false;
}

auto json1 = parseJSONTemplate!OrderedAAPolicy(...);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. However, now that std.json does not depend on having OrderedAA defined, these can be 2 separate changes. When I make a separate PR to add OrderedAA to std.array, we can remove this instance I've included in the unittest and swap it out for that.

Note that by separating these changes into distinct PRs, it makes this one easier to accept on it's own. Once this one gets accepted, we can start using this feature in dmd by simply including our own definition of OrderedAA until we can add it to std.array. I'm guessing that adding OrderedAA is going to be a fair amount of work with lots of revision and documentation so I would rather not have this change depend on that one.

@marler8997
Copy link
Contributor Author

Per @quickfur's comments, I've created a PR to remove the object field sorting that is currently present in std.json: #6084

@wilzbach
Copy link
Member

BTW here's what the ECMA15 spec says - it requires insertion order to be preserved (it didn't before).

order of enumerating the properties is not specified but must conform to the rules specified below.

Let keys be a new empty List.
For each own property key P of O that is an integer index, in ascending numeric index order
Add P as the last element of keys.
For each own property key P of O that is a String but is not an integer index, in property creation order
Add P as the last element of keys.
For each own property key P of O that is a Symbol, in property creation order
Add P as the last element of keys.
Return keys.

Granted, JS and JSON are different things, but people mix them up often enough, s.t. preserving insertion order is an interesting use case.

@marler8997
Copy link
Contributor Author

BTW here's what the ECMA15 spec says - it requires insertion order to be preserved (it didn't before).

Can't say I'm surprised by this change to ECMA15 :) Based on @quickfur 's comments I refactored this change to remove the option to preserve order, maybe I should bring it back in? I've currently been working on modifying this PR to appease @quickfur but after I refactored the change based on his comments he has been silent. This seems to be a pattern among some of dlang's developers and definitely not a good one.

@quickfur
Copy link
Member

@marler8997 Please understand that (1) I'm not paid for doing this; (2) I'm doing this entirely out of my free time; (3) my free time is very scant (my weekends are usually completely booked); (4) Phobos has tons of other PRs and things that I can look at; and (5) I'm not under contract to respond to stuff by some deadline. Basically, the only reason I even respond at all is only out of goodwill. So I don't understand why a pause of 4 days is unacceptable. If you paid me to work on this full-time, then I would respond within a couple of hours, but IMO the accusatory tone in your comment is uncalled for. If you feel the response rate of Phobos devs is not up to your standard, then perhaps you should consider donating to the D foundation to hire full-time programmers to work on this.

@marler8997
Copy link
Contributor Author

@quickfur

Sorry for offending you. I don't mean to say that you NEED to be more responsive in general. My point was that if you decide to engage in a PR with criticism that you should follow through when the person takes your criticism and spends their time refactoring their work based on your comments. Of course you're not under any obligation to respond quickly, but I do think it's inconsiderate to criticize someone else's work and then fail to respond in a timely manner after they've gone out of their way to redo all their work based on that criticism.

@quickfur
Copy link
Member

Well, your last change was 3 days ago, and as I said, my weekend are booked, so if you subtract that, that's really only 1 day. I don't think that's unreasonable at all, given that there's a whole bunch of other stuff I was reviewing, and my 1 day is really a lot less since I have a full-time job and can't be spending all day working on Phobos -- my employer would fire me. If I didn't respond for a week, then you may have a point, but if your expectations are that I have to respond instantly, then perhaps I should refrain from reviewing your PRs in the future since I can't keep up with your expectations.

@marler8997
Copy link
Contributor Author

@quickfur I'll know in the future to expect it to take up to a week for you to respond. Glad we've got that cleared up. Let's continue the more useful discussion at hand, namely, what to do with this PR.

Copy link
Member

@quickfur quickfur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this is a really big change. I'm going to have to defer to @andralex and the other reviewers whether or not to go with this.

My main concern is that this is a lot of change adding a lot of complexity just to support one use case (support the ability to swap in a different implementation of JSONObject). Surely there's a simpler way to implement this that doesn't require a changeset of this size? E.g., if JSONObject were made an interface instead, so that different concrete types can be hooked in without a renaming that affects everything else in JSONValue. Then it would just be a matter of having the user supply a factory method of some sort to construct the desired object type. There are probably other ways to implement this that doesn't entail such a massive change.

With a change this big for such a small feature, I'm afraid that even if I were to approve this PR, @andralex is likely to veto it.

alias MyObjectType(K,V) = OrderedAA!(K,V);
}
alias MyJSONValue = JSONValueTemplate!MyPolicy;
------------
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example here really should be in a ddoc'd unittest. And it should show how to actually use JSONValueTemplate, not just in how to instantiate it, but how to use it with the rest of the module, e.g., to configure it with a different JSONObject type, etc..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Was going to wait to fully document until a general "approved" consensus was reached.

@marler8997
Copy link
Contributor Author

marler8997 commented Jan 29, 2018

I'm going to have to disagree on the size of this change. Most of the lines the actual change are unit tests/documentation and renaming JSONValue to Value (internally). The only functional change is redifining the implementation of JSONValue, toJSON and parseJSON as templates with hooks to override functionality.

Also, saying that this only adds support for "one use case' is just not true. The change is providing a couple hooks to override to parts of the implementation, along with a framework for providing more hooks in the future.

As for @andralex, he may or may not approve but I'd be more interested in every one giving their own opinion rather than people trying to guess what others might think.

@quickfur
Copy link
Member

OK, then let's let @andralex decide whether or not to go ahead with this.

@andralex
Copy link
Member

andralex commented Jan 30, 2018

On a cursory look I see two issues with this:

  • Preserving order of fields is mentioned repeatedly as an important application, but relying on order of fields is specifically listed as an antipattern in JSON canon - see e.g. https://stackoverflow.com/questions/3948206/json-order-mixed-up
  • std.json is generally disliked. This feature addition does not improve on any of the dislikes.

It don't see how this can be brought to a solid addition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants