-
-
Notifications
You must be signed in to change notification settings - Fork 55
/
mapping.cr
143 lines (132 loc) · 4.73 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
module DB
# Empty module used for marking a class as supporting DB:Mapping
module Mappable; end
# The `DB.mapping` macro defines how an object is built from a DB::ResultSet.
#
# It takes hash literal as argument, in which attributes and types are defined.
# Once defined, `DB::ResultSet#read(t)` populates properties of the class from the
# result set.
#
# ```crystal
# require "db"
#
# class Employee
# DB.mapping({
# title: String,
# name: String,
# })
# end
#
# employees = Employee.from_rs(db.query("SELECT title, name FROM employees"))
# employees[0].title # => "Manager"
# employees[0].name # => "John"
# ```
#
# Attributes not mapped with `DB.mapping` are not defined as properties.
# Also, missing attributes raise a `DB::Exception`.
#
# You can also define attributes for each property.
#
# ```crystal
# class Employee
# DB.mapping({
# title: String,
# name: {
# type: String,
# nilable: true,
# key: "firstname",
# },
# })
# end
# ```
#
# Available attributes:
#
# * *type* (required) defines its type. In the example above, *title: String* is a shortcut to *title: {type: String}*.
# * *nilable* defines if a property can be a `Nil`.
# * **default**: value to use if the property is missing in the result set, or if it's `null` and `nilable` was not set to `true`. If the default value creates a new instance of an object (for example `[1, 2, 3]` or `SomeObject.new`), a different instance will be used each time a row is parsed.
# * *key* defines which column to read from a reusltset. It defaults to the name of the property.
# * *converter* takes an alternate type for parsing. It requires a `#from_rs` method in that class, and returns an instance of the given type.
#
# 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 `DB::ResultSet` that reads from
# it and initializes this type's instance variables.
#
# This macro also declares instance variables of the types given in the mapping.
macro mapping(properties, strict = true)
include ::DB::Mappable
{% for key, value in properties %}
{% properties[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
{% end %}
{% for key, value in properties %}
@{{key.id}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }}
def {{key.id}}=(_{{key.id}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }})
@{{key.id}} = _{{key.id}}
end
def {{key.id}}
@{{key.id}}
end
{% end %}
def self.from_rs(%rs : ::DB::ResultSet)
%objs = Array(self).new
%rs.each do
%objs << self.new(%rs)
end
%objs
end
def initialize(%rs : ::DB::ResultSet)
{% for key, value in properties %}
%var{key.id} = nil
%found{key.id} = false
{% end %}
%rs.each_column do |col_name|
case col_name
{% for key, value in properties %}
when {{value[:key] || key.id.stringify}}
%found{key.id} = true
%var{key.id} =
{% if value[:converter] %}
{{value[:converter]}}.from_rs(%rs)
{% elsif value[:nilable] || value[:default] != nil %}
%rs.read(Union({{value[:type]}} | Nil))
{% else %}
%rs.read({{value[:type]}})
{% end %}
{% end %}
else
{% if strict %}
raise ::DB::MappingException.new("unknown result set attribute: #{col_name}")
{% else %}
%rs.read
{% end %}
end
end
{% for key, value in properties %}
{% unless value[:nilable] || value[:default] != nil %}
if %var{key.id}.is_a?(Nil) && !%found{key.id}
raise ::DB::MappingException.new("missing result set attribute: {{(value[:key] || key).id}}")
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}.is_a?(Nil) ? {{value[:default]}} : %var{key.id}
{% else %}
@{{key.id}} = %var{key.id}.not_nil!
{% end %}
{% end %}
end
end
macro mapping(**properties)
::DB.mapping({{properties}})
end
end