-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 JSON.def_to_json #4772
Add JSON.def_to_json #4772
Conversation
src/json/def_to_json.cr
Outdated
json.object do | ||
{% for key, options in mappings %} | ||
{% unless options.is_a?(HashLiteral) || options.is_a?(NamedTupleLiteral) %} | ||
{% options = {nil: nil} %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks ugly but I couldn't find a way to define an empty NamedTupleLiteral. Suggestions are welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@straight-shoota NamedTuple.new
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RX14 won't work: https://carc.in/#/r/2iyw, @straight-shoota wants to assign a literal, so it can be used as a container later in the macro
2ca0946
to
a6e9b93
Compare
CI checks are failing because of a formatter issue. I couldn't isolate the reason for this but it looks like it might be related to #4769 - at least it occurs at a similar location, inside |
@straight-shoota done in #4801. |
4e585fa
to
693482b
Compare
Rebased on master with #4801. Builds are still failing because the last commit removes the workaround and the default compiler (0.23.1) does not have the fix from #4801. The compiler needs to be built from current master. I guess this won't work without nightly builds. Should I keep the workaround in for now? @RX14 What do you think about this PR in general? |
I don't understand the point of this addition, or how it relates to Also, all releases must be buildable by the last release. Nobody is going to merge this before it passes CI. |
693482b
to
6cf8f50
Compare
Alright, so I'll keep the workaround until the next compiler version is released.
For some usecases (like exporting the compiler's type and method definitions to json) there is no need to define ivars nor a parser which would both come with This functionality was for the most part already implemented in |
6cf8f50
to
dd1c264
Compare
src/json/def_to_json.cr
Outdated
# whose keys will define JSON properties. | ||
# | ||
# The value of each key can either be `true` or a hash or named tuple literal with the following options: | ||
# * **property**: the property name on the Crystal object (as opposed to the key in the in the JSON document) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: in the in the
src/json/def_to_json.cr
Outdated
end | ||
|
||
# :nodoc: | ||
macro field_to_json(value_name, field_name, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this macro name and its arguments are inconsistent:
field_to_json
: you want to generate the code that will transform the crystal-field to json. So herefield == crystal-field
.value_name
: wut?field_name
: from the macro name, we could understand that it's thecrystal-field
name. In fact it's thejson-key
name.
I think that to keep things coherent you can either:
- Change the macro name to
value_to_json
- Change the argument
field_name
tokey
orjson_key
, and the argumentvalue_name
tofield
orfield_name
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I did this a bit sloppy because it is only an internal macro and was refactored quite a few times... ;)
But I like your suggestions, thanks for reviewing!
- I think I'll change the macro name to
emit_value_to_json
- it's better to have a verb. - I am not sure about
field_name
because it is technically not always referring to a field but might as well be an ordinary method returning a value unrelated to an instance variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll change the macro name to
emit_value_to_json
- it's better to have a verb.
right, it's good too!
I am not sure about
field_name
because it is technically not always referring to a field but might as well be an ordinary method returning a value unrelated to an instance variable.
True, then maybe getter_name
? but that's not ok with @var
accessor. or just accessor_name
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained below it does not necessarily have to refer to a getter or any method on self
but may in fact be any expression. So maybe better just use value_expression
?
src/json/def_to_json.cr
Outdated
::JSON.def_to_json({{type}}, {{mappings}}) | ||
end | ||
|
||
# The `StringConverter` has a class method `to_json` wich can be used as a converter for `JSON.def_to_json`. The value is added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: wich
=> which
src/json/def_to_json.cr
Outdated
# whose keys will define JSON properties. | ||
# | ||
# The value of each key can either be `true` or a hash or named tuple literal with the following options: | ||
# * **property**: the property name on the Crystal object (as opposed to the key in the JSON document) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current JSON.mapping
macro, one specify the crystal's property name as key in the properties
arg (your mappings
arg), then you can specify an optional key
if the JSON field name is different.
Here you did it the other way around, why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense this way: with def_to_json
you're basically describing a JSON object schema. The keys of this schema correspond to the keys of the named tuple and the value defines which value is to be assigned.
mapping
on the other hand has a different focus: it primarily defines properties as instance variables and adds methods to convert them to JSON and back. Here it makes sense to have the crystal properties as keys (because that's the main part) and the JSON field names as arguments.
While the properties
argument looks quite similar, both macros have a different usecase and allow different options, so I don't think it is bad to have different semantics between key and value.
For def_to_json
there is also the benefit that this way it is very easy to use arbitrary expressions as value:
@name = "FooBar"
JSON.def_to_json({
name: { property: name.downcase }
})
# to_json will output {"name":"foobar"}
This would not be possible or look very nasty if it was used as a key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this feature should also be noted in the documentation...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I'm not sure I completely agree, but it makes sense, and given this concept differences I think it's acceptable (and interesting to have too!).
Also I think you should copy/paste this discussion (only the important bits: question & answer?) in the issue's conversation, so your explaination about the differences of concept can't get lost as a line comment when/if you change this particular line of code.
I would suggest @name = "FooBar"
JSON.def_to_json({
name: { value: name.downcase }
}) And to simplify a bit: as in |
I've thought about that simplification. It's just that then it's difficult to have a shortcut for the case that JSON key and property name are equal. This is currently encoded by the value |
Copied from line comments by @bew (question) and me (answer) so it doesn't get lost:
I think it makes more sense this way: with For @name = "FooBar"
JSON.def_to_json({
name: { value: name.downcase }
})
# to_json will output {"name":"foobar"} This would not be possible or look very nasty if it was used as a key. |
@RX14 there have been a few CI builds where only linux64 failed with |
About the simplification, here an example of what you could use: JSON.def_to_json({
element: _, # Use element
node: hello.node_getter, # Use hello.node_getter
stuff: _, # Use stuff
}) Example: https://carc.in/#/r/2j3c I find it pretty readable, with no duplication, and this could allow the simplification I talked about earlier! |
@bew That looks like a great idea! |
Does this PR need any additional work? |
…tailed instructions for removal
fb76538
to
ff40766
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I think the documentation needs to be improved, it's dry and at times misleading.
src/json/def_to_json.cr
Outdated
module JSON | ||
# The `def_to_json` macro generates a `to_json(json : JSON::Builder)` method based on provided *mappings*. | ||
# | ||
# It is a lightweight alternative to `JSON.mapping` if you don't need to declare instance variables and a parser. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd phrase this in terms of "serialization-only" or "read-only" instead of "if you don't need to declare instance variables and a parser" because your docs don't get near the heart of what def_to_json
is used for.
src/json/def_to_json.cr
Outdated
# | ||
# It is a lightweight alternative to `JSON.mapping` if you don't need to declare instance variables and a parser. | ||
# | ||
# The generated method invoks `to_json(JSON::Builder)` on each of the values returned by the *value* expression, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what "the value expression" is is undefined before this point.
src/json/def_to_json.cr
Outdated
# require "json" | ||
# | ||
# record Location, lat : Float64, long : Float64 do | ||
# JSON.def_to_json([lat, long]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this array stuff defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I forgot to change this. The array syntax was not really useful.
src/json/def_to_json.cr
Outdated
# * **converter**: specify an alternate type for generation. The converter must define `to_json(value, JSON::Builder)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`. | ||
# * **root**: assume the value is inside a JSON object with a given key | ||
# | ||
# If it is not a hash or named tuple literal, the expression will be interpreted as `value` parameter. As a shortcut `_` represents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about the _
shortcut, I think foo: foo
is perhaps the clearest. I can see it getting terrible with long names though.
Just a note that a more granular API isn't always good. Look at Java, for example. Many ways to do the same things is also confusing. I don't understand why I am personally not sure about this. |
@asterite this is for the case where you don't want to serialize all the instance variables, only a subset, so the generated initialize would be invalid |
@asterite The main problem with For def to_json(builder : JSON::Builder)
MethodExport.new(self).to_json(builder)
end
struct MethodExport
JSON.mapping(
id: String,
html_id: String,
name: String,
doc: String,
summary: String,
abstract: String,
args: String,
args_string: String,
source_link: String,
def: String
)
def initialize(method)
@id = method.id
@html_id = method.html_id
@name = method.name
# ...
end
end |
You can always do: def to_json(builder : JSON::Builder)
builder.object do
builder.field "name", @name
builder.field "age", @age
end
end There's no need to create an extra object with the mapping. I'm still not sure introducing a huge refactor and more public API methods is good in this case. |
Also, once we introduce meta attributes for instance variables, all of this will be kind of obsolete. You'll just specify which instance variables should be serialized, and how. That will work for generating JSON and for parsing, like in every other programming language. So I wouldn't continue making changes to JSON just yet. |
Yes, manually writing to a |
Personally I don't mind the manual |
Okay, I'll try this for #4746 |
I don't think this is worth pursuing anymore, given that building the JSON manually is pretty straightforward. |
This puts the
def to_json
generator fromJSON.mapping
in a separate macro to make it individually accessible where automatically declared instance variables and parser are not needed. This comes in two flavors: one for convertingself
to JSON (this is the same asJSON.mapping
without the extra features) and one which accepts the object-to-be-converted as an argument - this is useful to define JSON converters.Further documentation and specs are included.
This PR was extracted from #4746 which also shows an exemplary usecase.