field_mapper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/field_mapper.rb +18 -0
- data/lib/field_mapper/custom/converter.rb +118 -0
- data/lib/field_mapper/custom/field.rb +110 -0
- data/lib/field_mapper/custom/plat.rb +60 -0
- data/lib/field_mapper/custom/value.rb +41 -0
- data/lib/field_mapper/errors.rb +11 -0
- data/lib/field_mapper/marshaller.rb +24 -0
- data/lib/field_mapper/name_helper.rb +17 -0
- data/lib/field_mapper/standard/converter.rb +111 -0
- data/lib/field_mapper/standard/field.rb +157 -0
- data/lib/field_mapper/standard/plat.rb +279 -0
- data/lib/field_mapper/standard/value.rb +24 -0
- data/lib/field_mapper/types/boolean.rb +15 -0
- data/lib/field_mapper/types/list.rb +58 -0
- data/lib/field_mapper/types/plat.rb +35 -0
- data/lib/field_mapper/version.rb +3 -0
- data/test/custom/converter_test.rb +123 -0
- data/test/custom/field_test.rb +137 -0
- data/test/custom/plat_example.rb +58 -0
- data/test/custom/plat_example_alt.rb +34 -0
- data/test/custom/plat_test.rb +102 -0
- data/test/custom/value_test.rb +43 -0
- data/test/readme_test.rb +119 -0
- data/test/standard/converter_test.rb +88 -0
- data/test/standard/field_test.rb +156 -0
- data/test/standard/plat_example.rb +48 -0
- data/test/standard/plat_test.rb +304 -0
- data/test/standard/value_test.rb +28 -0
- data/test/test_helper.rb +15 -0
- data/test/types/boolean_test.rb +77 -0
- data/test/types/list_test.rb +71 -0
- data/test/types/plat_test.rb +38 -0
- metadata +246 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require "csv"
|
2
|
+
require "oj"
|
3
|
+
require_relative "value"
|
4
|
+
|
5
|
+
module FieldMapper
|
6
|
+
module Standard
|
7
|
+
class Field
|
8
|
+
include FieldMapper::Marshaller
|
9
|
+
|
10
|
+
attr_reader(
|
11
|
+
:name,
|
12
|
+
:type,
|
13
|
+
:desc,
|
14
|
+
:default,
|
15
|
+
:values
|
16
|
+
)
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
name,
|
20
|
+
type: nil,
|
21
|
+
desc: nil,
|
22
|
+
default: nil
|
23
|
+
)
|
24
|
+
raise TypeNotSpecified.new("type not specified for: #{name}") if type.nil?
|
25
|
+
@name = name.to_sym
|
26
|
+
@type = type
|
27
|
+
@desc= desc
|
28
|
+
@default = default
|
29
|
+
end
|
30
|
+
|
31
|
+
def list?
|
32
|
+
type.name == "FieldMapper::Types::List"
|
33
|
+
end
|
34
|
+
|
35
|
+
def plat?
|
36
|
+
type.name == "FieldMapper::Types::Plat"
|
37
|
+
end
|
38
|
+
|
39
|
+
def plat_field?
|
40
|
+
plat? || plat_list?
|
41
|
+
end
|
42
|
+
|
43
|
+
def plat_list?
|
44
|
+
list? && type.plat_list?
|
45
|
+
end
|
46
|
+
|
47
|
+
def raw_values
|
48
|
+
return nil unless has_values?
|
49
|
+
values.map { |v| v.value }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adds a value to a Field instance.
|
53
|
+
# Intended use is from within a Plat class declaration.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# class ExamplePlat < FieldMapper::Standard::Plat
|
57
|
+
# field :example do
|
58
|
+
# value 1
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
def value(val)
|
62
|
+
@values ||= []
|
63
|
+
@values << FieldMapper::Standard::Value.new(val, field: self)
|
64
|
+
@values.last
|
65
|
+
end
|
66
|
+
|
67
|
+
# Adds values to a Field instance that are defined in a CSV file.
|
68
|
+
#
|
69
|
+
# Intended use is from within a Plat class declaration.
|
70
|
+
# @example
|
71
|
+
# class ExamplePlat < FieldMapper::Standard::Plat
|
72
|
+
# field :example do
|
73
|
+
# load_values "/path/to/file.csv"
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# The format of the CSV file should contain a single column with a header row.
|
78
|
+
# @example
|
79
|
+
# Name of Field
|
80
|
+
# 1
|
81
|
+
# 2
|
82
|
+
#
|
83
|
+
def load_values(path_to_csv)
|
84
|
+
CSV.foreach(path_to_csv, :headers => true) do |row|
|
85
|
+
value row["standard_value"].to_s.strip
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def has_values?
|
90
|
+
!values.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
def find_value(value)
|
94
|
+
return nil unless has_values?
|
95
|
+
values.find { |val| val == value || val.value == value }
|
96
|
+
end
|
97
|
+
|
98
|
+
def cast(value, as_single_value: false)
|
99
|
+
value = cast_value(type, value, as_single_value: as_single_value)
|
100
|
+
return nil if value.nil? || value.to_s.blank?
|
101
|
+
value = clean_value(value) unless as_single_value
|
102
|
+
value
|
103
|
+
end
|
104
|
+
|
105
|
+
alias_method :to_s, :name
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def cast_value(type, value, as_single_value: false)
|
110
|
+
return nil if value.nil?
|
111
|
+
case type.name
|
112
|
+
when "String" then return value.to_s.strip
|
113
|
+
when "FieldMapper::Types::Boolean" then return FieldMapper::Types::Boolean.parse(value)
|
114
|
+
when "Time" then return Time.parse(value.to_s) rescue nil # TODO: log error?
|
115
|
+
when "Integer" then return value.to_i
|
116
|
+
when "Float" then return value.to_f
|
117
|
+
when "Money" then return Monetize.parse(value) rescue nil # TODO: log error?
|
118
|
+
when "FieldMapper::Types::Plat" then return plat_instance(type, value)
|
119
|
+
when "FieldMapper::Types::List" then
|
120
|
+
return value if value.is_a?(Array) && value.empty?
|
121
|
+
return plat_instance_list(type, value) if type.plat_list?
|
122
|
+
return cast_value(type.type, value) if as_single_value
|
123
|
+
get_list value
|
124
|
+
else
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_list(value)
|
130
|
+
value = unmarshal(value) if value.is_a?(String)
|
131
|
+
value.map { |val| cast_value(type.type, val) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def clean_value(value)
|
135
|
+
return value unless has_values?
|
136
|
+
return value unless type.name == "FieldMapper::Types::List"
|
137
|
+
value & raw_values
|
138
|
+
end
|
139
|
+
|
140
|
+
def plat_instance(type, value)
|
141
|
+
return value if value.is_a?(FieldMapper::Standard::Plat)
|
142
|
+
return value if value.is_a?(Numeric)
|
143
|
+
return value.to_i if value.is_a?(String) && value =~ /\A\d+\z/
|
144
|
+
return type.type.new(value) if value.is_a?(Hash)
|
145
|
+
return type.type.new(unmarshal(value)) if value.is_a?(String)
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def plat_instance_list(type, value)
|
150
|
+
return value if value.empty?
|
151
|
+
value = unmarshal(value) if value.is_a?(String)
|
152
|
+
return value.map { |val| plat_instance(type, val) }
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
require_relative "../errors"
|
3
|
+
require_relative "../name_helper"
|
4
|
+
require_relative "../marshaller"
|
5
|
+
require_relative "field"
|
6
|
+
|
7
|
+
module FieldMapper
|
8
|
+
module Standard
|
9
|
+
class Plat
|
10
|
+
include FieldMapper::NameHelper
|
11
|
+
include FieldMapper::Marshaller
|
12
|
+
|
13
|
+
class << self
|
14
|
+
include FieldMapper::NameHelper
|
15
|
+
|
16
|
+
def fields
|
17
|
+
@fields ||= HashWithIndifferentAccess.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def field_names
|
21
|
+
@field_names ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def field(
|
25
|
+
name,
|
26
|
+
type: nil,
|
27
|
+
desc: nil,
|
28
|
+
default: nil,
|
29
|
+
&block
|
30
|
+
)
|
31
|
+
field_names[attr_name(name)] = name
|
32
|
+
|
33
|
+
field = fields[name] = FieldMapper::Standard::Field.new(
|
34
|
+
name,
|
35
|
+
type: type,
|
36
|
+
desc: desc,
|
37
|
+
default: default
|
38
|
+
)
|
39
|
+
|
40
|
+
field.instance_exec(&block) if block_given?
|
41
|
+
|
42
|
+
define_method(attr_name name) do
|
43
|
+
self[name]
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method("#{attr_name name}=") do |value|
|
47
|
+
self[name] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_field(field_name)
|
52
|
+
fields[field_names[attr_name(field_name)]]
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_plat_fields?
|
56
|
+
!plat_fields.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def plat_fields
|
60
|
+
fields.reduce({}) do |memo, keypair|
|
61
|
+
if keypair.last.plat_field?
|
62
|
+
memo[keypair.first] = keypair.last
|
63
|
+
end
|
64
|
+
memo
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :node_id
|
71
|
+
|
72
|
+
def initialize(params={})
|
73
|
+
@node_id = params["_node_id"]
|
74
|
+
assign_defaults
|
75
|
+
assign_params params
|
76
|
+
end
|
77
|
+
|
78
|
+
def scope
|
79
|
+
@scope ||= begin
|
80
|
+
scope_name = self.class.name.split("::")[0..-2].join("::")
|
81
|
+
if scope_name.empty?
|
82
|
+
::Object
|
83
|
+
else
|
84
|
+
::Object.const_get(scope_name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def after_convert(from: nil, to: nil)
|
90
|
+
# abstract method to be implemented by subclasses
|
91
|
+
end
|
92
|
+
|
93
|
+
def [](field_name)
|
94
|
+
raise FieldNotDefined unless field_exists?(field_name)
|
95
|
+
instance_variable_get "@#{attr_name(field_name)}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def []=(field_name, value)
|
99
|
+
field = self.class.find_field(field_name)
|
100
|
+
raise FieldNotDefined if field.nil?
|
101
|
+
instance_variable_set "@#{attr_name(field_name)}", field.cast(value)
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_hash(flatten: false, history: {}, include_meta: true)
|
105
|
+
history[object_id] = true
|
106
|
+
hash = self.class.fields.values.reduce(HashWithIndifferentAccess.new) do |memo, field|
|
107
|
+
name = field.name
|
108
|
+
value = instance_variable_get("@#{attr_name(name)}")
|
109
|
+
|
110
|
+
if value.present?
|
111
|
+
case field.type.name
|
112
|
+
when "FieldMapper::Types::Plat" then
|
113
|
+
if value.is_a? FieldMapper::Standard::Plat
|
114
|
+
oid = value.object_id
|
115
|
+
if history[oid].nil?
|
116
|
+
history[oid] = true
|
117
|
+
value = value.to_hash(flatten: flatten, history: history, include_meta: include_meta)
|
118
|
+
value = marshal(value) if flatten
|
119
|
+
else
|
120
|
+
value = oid
|
121
|
+
end
|
122
|
+
else
|
123
|
+
value
|
124
|
+
end
|
125
|
+
when "FieldMapper::Types::List" then
|
126
|
+
if field.plat_list?
|
127
|
+
value = value.map do |val|
|
128
|
+
if val.is_a? FieldMapper::Standard::Plat
|
129
|
+
oid = val.object_id
|
130
|
+
if history[oid].nil?
|
131
|
+
history[oid] = true
|
132
|
+
val.to_hash(flatten: flatten, history: history, include_meta: include_meta)
|
133
|
+
else
|
134
|
+
oid
|
135
|
+
end
|
136
|
+
else
|
137
|
+
val
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
value = marshal(value) if flatten
|
142
|
+
when "Money" then
|
143
|
+
value = value.format(with_currency: true)
|
144
|
+
when "Time" then
|
145
|
+
value = value.utc.iso8601
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
memo[name] = value
|
150
|
+
memo
|
151
|
+
end
|
152
|
+
|
153
|
+
if include_meta
|
154
|
+
hash = {
|
155
|
+
_node_id: object_id,
|
156
|
+
_flat: flatten
|
157
|
+
}.merge(hash)
|
158
|
+
end
|
159
|
+
|
160
|
+
HashWithIndifferentAccess.new(hash)
|
161
|
+
end
|
162
|
+
|
163
|
+
def cache_key
|
164
|
+
self.class.name + "-" + Digest::MD5.hexdigest(to_hash.to_s)
|
165
|
+
end
|
166
|
+
|
167
|
+
protected
|
168
|
+
|
169
|
+
def plat_values
|
170
|
+
self.class.plat_fields.values.reduce([]) do |memo, field|
|
171
|
+
value = send(attr_name(field.name))
|
172
|
+
memo << value if field.plat?
|
173
|
+
memo.concat value if field.plat_list? && !value.nil?
|
174
|
+
memo
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def descendant_plats(history: {})
|
179
|
+
return {} if history[object_id]
|
180
|
+
history[object_id] = true
|
181
|
+
plats = {}
|
182
|
+
plats[self.node_id || self.object_id] = self
|
183
|
+
|
184
|
+
plat_values.each do |plat|
|
185
|
+
if plat.is_a? FieldMapper::Standard::Plat
|
186
|
+
plats[plat.node_id || plat.object_id] = plat
|
187
|
+
plats.merge! plat.descendant_plats(history: history)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
plats
|
192
|
+
end
|
193
|
+
|
194
|
+
def pending_assignments
|
195
|
+
@pending_assignments ||= []
|
196
|
+
end
|
197
|
+
|
198
|
+
def all_pending_assignments
|
199
|
+
descendant_plats.values.reduce([]) do |memo, plat|
|
200
|
+
memo.concat plat.pending_assignments
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def field_exists?(field_name)
|
205
|
+
!!self.class.field_names[attr_name(field_name)]
|
206
|
+
end
|
207
|
+
|
208
|
+
def assign_defaults
|
209
|
+
self.class.fields.each do |name, field|
|
210
|
+
if !field.default.nil?
|
211
|
+
value = field.default
|
212
|
+
value = value.clone rescue value
|
213
|
+
instance_variable_set "@#{attr_name(field.name)}", field.cast(value)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def assign_params(params)
|
219
|
+
pending_assignments.clear
|
220
|
+
params.each do |name, value|
|
221
|
+
field = self.class.fields[name]
|
222
|
+
next if field.nil?
|
223
|
+
value = field.cast(value)
|
224
|
+
next if value.nil?
|
225
|
+
next if field.list? && value.compact.empty?
|
226
|
+
assign_param name, value
|
227
|
+
add_pending_assignment field, value
|
228
|
+
end
|
229
|
+
|
230
|
+
apply_pending_assignments descendant_plats
|
231
|
+
end
|
232
|
+
|
233
|
+
def assign_param(name, value)
|
234
|
+
instance_variable_set "@#{attr_name(name)}", value
|
235
|
+
end
|
236
|
+
|
237
|
+
def add_pending_assignment(field, value)
|
238
|
+
if field.plat? && value.is_a?(Numeric)
|
239
|
+
add_pending_assignment_for_plat field, value
|
240
|
+
end
|
241
|
+
|
242
|
+
if field.plat_list? && !value.nil?
|
243
|
+
add_pending_assignment_for_plat_list field, value
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def add_pending_assignment_for_plat(field, value)
|
248
|
+
pending_assignments << lambda do |descendant_plats|
|
249
|
+
if value.is_a?(Numeric)
|
250
|
+
plat = descendant_plats[value]
|
251
|
+
assign_param field.name, plat unless plat.nil?
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def add_pending_assignment_for_plat_list(field, value)
|
257
|
+
pending_assignments << lambda do |descendant_plats|
|
258
|
+
list = value.reduce([]) do |memo, val|
|
259
|
+
if val.is_a?(Numeric)
|
260
|
+
plat = descendant_plats[val]
|
261
|
+
memo << (plat ? plat : val)
|
262
|
+
else
|
263
|
+
memo << val
|
264
|
+
end
|
265
|
+
memo
|
266
|
+
end
|
267
|
+
assign_param field.name, list
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def apply_pending_assignments(descendant_plats)
|
272
|
+
all_pending_assignments.each do |assignment|
|
273
|
+
assignment.call descendant_plats
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "../errors"
|
2
|
+
|
3
|
+
module FieldMapper
|
4
|
+
module Standard
|
5
|
+
class Value
|
6
|
+
attr_reader(
|
7
|
+
:value,
|
8
|
+
:field
|
9
|
+
)
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
value,
|
13
|
+
field: nil
|
14
|
+
)
|
15
|
+
raise ArgumentError.new("field is required") if
|
16
|
+
field.nil?
|
17
|
+
|
18
|
+
@field = field
|
19
|
+
@value = field.cast(value, as_single_value: true)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|