Skip to content

Instantly share code, notes, and snippets.

@bew
Created March 13, 2017 04:52
Show Gist options
  • Save bew/2af1c4c4bd8e03a5247fba9eeb83a6a9 to your computer and use it in GitHub Desktop.
Save bew/2af1c4c4bd8e03a5247fba9eeb83a6a9 to your computer and use it in GitHub Desktop.
A JSON.mapper for crystal that allow additional fields
module JSON
# JSON.mapping documentation removed, as it's taking to much spaces :p
# prop: the properties, like JSON.mapping
# decl: whatever or not to generate the variables declarations, getters & setters
# more_init: if not nil, must be a TupleLiteral that is the list of additional fields to the initialize method
macro mapper(props properties, strict = false, decl gen_declarations = true, more_init = nil)
{% for key, value in properties %}
{% properties[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
{% end %}
{% if gen_declarations %}
{% for key, value in properties %}
@{{key.id}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }}
{% if value[:setter] == nil ? true : value[:setter] %}
def {{key.id}}=(_{{key.id}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }})
@{{key.id}} = _{{key.id}}
end
{% end %}
{% if value[:getter] == nil ? true : value[:getter] %}
def {{key.id}}
@{{key.id}}
end
{% end %}
{% end %}
{% end %}
def initialize(%pull : ::JSON::PullParser,
{% if more_init && more_init.is_a?(TupleLiteral) && more_init.size > 0 %}
{% for var in more_init %}
@{{var.id}}
{% end %}
{% end %}
)
{% for key, value in properties %}
%var{key.id} = nil
%found{key.id} = false
{% end %}
%pull.read_object do |key|
case key
{% for key, value in properties %}
when {{value[:key] || key.id.stringify}}
%found{key.id} = true
%var{key.id} =
{% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %}
{% if value[:root] %}
%pull.on_key!({{value[:root]}}) do
{% end %}
{% if value[:converter] %}
{{value[:converter]}}.from_json(%pull)
{% elsif value[:type].is_a?(Path) || value[:type].is_a?(Generic) %}
{{value[:type]}}.new(%pull)
{% else %}
::Union({{value[:type]}}).new(%pull)
{% end %}
{% if value[:root] %}
end
{% end %}
{% if value[:nilable] || value[:default] != nil %} } {% 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] || value[:default] != nil %}
if %var{key.id}.nil? && !%found{key.id} && !::Union({{value[:type]}}).nilable?
raise ::JSON::ParseException.new("Missing json attribute: {{(value[:key] || key).id}}", 0, 0)
end
{% end %}
{% end %}
{% for key, value in properties %}
{% if value[:nilable] %}
{% if value[:default] != nil %}
@{{key.id}} = %found{key.id} ? %var{key.id} : {{value[:default]}}
{% else %}
@{{key.id}} = %var{key.id}
{% end %}
{% elsif value[:default] != nil %}
@{{key.id}} = %var{key.id}.nil? ? {{value[:default]}} : %var{key.id}
{% else %}
@{{key.id}} = (%var{key.id}).as({{value[:type]}})
{% end %}
{% end %}
end
def to_json(json : ::JSON::Builder)
json.object do
{% for key, value in properties %}
_{{key.id}} = @{{key.id}}
{% unless value[:emit_null] %}
unless _{{key.id}}.nil?
{% end %}
json.field({{value[:key] || key.id.stringify}}) do
{% if value[:root] %}
{% if value[:emit_null] %}
if _{{key.id}}.nil?
nil.to_json(json)
else
{% end %}
json.object do
json.field({{value[:root]}}) do
{% end %}
{% if value[:converter] %}
if _{{key.id}}
{{ value[:converter] }}.to_json(_{{key.id}}, json)
else
nil.to_json(json)
end
{% else %}
_{{key.id}}.to_json(json)
{% end %}
{% if value[:root] %}
{% if value[:emit_null] %}
end
{% end %}
end
end
{% end %}
end
{% unless value[:emit_null] %}
end
{% end %}
{% end %}
end
end
end
# This is a convenience method to allow invoking `JSON.mapping`
# with named arguments instead of with a hash/named-tuple literal.
macro mapper(**properties)
::JSON.mapper({{properties}})
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment