field_mapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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