-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathmapping.cr
147 lines (134 loc) · 4.75 KB
/
mapping.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# The `JSON::Mapping` module defines a single macro, `json_mapping`, that
# defines how an object is mapped to JSON.
#
# This module is automatically included by `Object` when you `require "json"`.
#
# ### Example
#
# ```
# require "json"
#
# class Location
# json_mapping({
# lat: Float64,
# lng: Float64,
# })
# end
#
# class House
# json_mapping({
# address: String,
# location: {type: Location, nilable: true},
# })
# end
#
# house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
# house.address #=> "Crystal Road 1234"
# house.location #=> #<Location:0x10cd93d80 @lat=12.3, @lng=34.5>
# house.to_json #=> %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}})
# ```
#
# ### Usage
#
# `json_mapping` must receive a hash literal whose keys will define Crystal properties.
#
# The value of each key can be a single type (never a union type). Primitive types (numbers, string, boll and nil)
# are support, as well as custom objects which must either use `json_mapping` or define a `new` method
# that accepts a `JSON::PullParser` and returns an object from it.
#
# The value can also be another hash literal with the following options:
# * type: (required) the single type described above
# * key: the property name in the JSON document (as opposed to the property name in the Crystal code)
# * nilable: if true, the property can be `Nil`
# * emit_null: if true, emits a `null` value for nilable properties (by default nulls are not emitted)
# * converter: specify an alternate type for parsing and generation. The converter must define `from_json(JSON::PullParser)` and `to_json(value, IO)` as class methods.
#
# The mapping also automatically defines Crystal properties (getters and setters) for each
# of the keys. It doesn't define a constructor accepting those arguments, but you can provide
# an overload.
#
# The macro basically defines a constructor accepting a `JSON::PullParser` that reads from
# it and initializes this type's instance variables. It also defines a `to_json(IO)` method
# by invoking `to_json(IO)` on each of the properties (unless a converter is specified, in
# which case `to_json(value, IO)` is invoked).
module JSON::Mapping
# Defines a JSON mapping. If `strict` is true, uknown properties in the JSON
# document will raise a parse exception. The default is `false`, so uknown properties
# are silently ignored.
macro json_mapping(properties, strict = false)
{% for key, value in properties %}
{% properties[key] = {type: value} unless value.is_a?(HashLiteral) %}
{% end %}
{% for key, value in properties %}
def {{key.id}}=(_{{key.id}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }})
@{{key.id}} = _{{key.id}}
end
def {{key.id}}
@{{key.id}}
end
{% end %}
def initialize(_pull : JSON::PullParser)
{% for key, value in properties %}
_{{key.id}} = nil
{% end %}
_pull.read_object do |_key|
case _key
{% for key, value in properties %}
when {{value[:key] || key.id.stringify}}
_{{key.id}} =
{% if value[:nilable] == true %} _pull.read_null_or { {% end %}
{% if value[:converter] %}
{{value[:converter]}}.from_json(_pull)
{% else %}
{{value[:type]}}.new(_pull)
{% end %}
{% if value[:nilable] == true %} } {% end %}
{% end %}
else
{% if strict %}
raise JSON::ParseException.new("unknown json attribute: #{_key}", 0, 0)
{% else %}
_pull.skip
{% end %}
end
end
{% for key, value in properties %}
{% unless value[:nilable] %}
if _{{key.id}}.is_a?(Nil)
raise JSON::ParseException.new("missing json attribute: {{(value[:key] || key).id}}", 0, 0)
end
{% end %}
{% end %}
{% for key, value in properties %}
@{{key.id}} = _{{key.id}}
{% end %}
end
def to_json(io : IO)
io.json_object do |json|
{% for key, value in properties %}
_{{key.id}} = @{{key.id}}
{% unless value[:emit_null] %}
unless _{{key.id}}.is_a?(Nil)
{% end %}
json.field({{value[:key] || key.id.stringify}}) do
{% if value[:converter] %}
if _{{key.id}}
{{ value[:converter] }}.to_json(_{{key.id}}, io)
else
nil.to_json(io)
end
{% else %}
_{{key.id}}.to_json(io)
{% end %}
end
{% unless value[:emit_null] %}
end
{% end %}
{% end %}
end
end
end
end
class Object
include JSON::Mapping
end