Class: Praxis::Mapper::Model

Inherits:
Object
  • Object
show all
Extended by:
Finalizable
Defined in:
lib/praxis-mapper/model.rb,
lib/praxis-mapper/support/factory_bot.rb

Constant Summary collapse

InspectedFields =
[:@data, :@deserialized_data].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Finalizable

_finalize!, extended, finalizable, finalize!, finalized?, inherited

Constructor Details

#initialize(data = {}) ⇒ Model

Returns a new instance of Model.



384
385
386
387
388
# File 'lib/praxis-mapper/model.rb', line 384

def initialize(data)
  @data = data
  @deserialized_data = {}
  @query = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



404
405
406
407
408
409
410
411
# File 'lib/praxis-mapper/model.rb', line 404

def method_missing(name, *args)
  if @data.has_key? name
    self.class.define_data_accessor(name)
    self.send(name)
  else
    super
  end
end

Class Attribute Details

._identitiesObject

Returns the value of attribute _identities.



12
13
14
# File 'lib/praxis-mapper/model.rb', line 12

def _identities
  @_identities
end

.associationsObject (readonly)

Returns the value of attribute associations.



13
14
15
# File 'lib/praxis-mapper/model.rb', line 13

def associations
  @associations
end

.configObject (readonly)

Returns the value of attribute config.



13
14
15
# File 'lib/praxis-mapper/model.rb', line 13

def config
  @config
end

.contextsObject (readonly)

Returns the value of attribute contexts.



13
14
15
# File 'lib/praxis-mapper/model.rb', line 13

def contexts
  @contexts
end

.serialized_fieldsObject (readonly)

Returns the value of attribute serialized_fields.



13
14
15
# File 'lib/praxis-mapper/model.rb', line 13

def serialized_fields
  @serialized_fields
end

Instance Attribute Details

#_queryObject

Returns the value of attribute _query.



9
10
11
# File 'lib/praxis-mapper/model.rb', line 9

def _query
  @_query
end

#_resourceObject

Returns the value of attribute _resource.



9
10
11
# File 'lib/praxis-mapper/model.rb', line 9

def _resource
  @_resource
end

#identity_mapObject

Returns the value of attribute identity_map.



9
10
11
# File 'lib/praxis-mapper/model.rb', line 9

def identity_map
  @identity_map
end

#new_recordObject

Returns the value of attribute new_record.



75
76
77
# File 'lib/praxis-mapper/support/factory_bot.rb', line 75

def new_record
  @new_record
end

Class Method Details

._finalize!Object

Internal finalize! logic



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/praxis-mapper/model.rb', line 31

def self._finalize!
  self.define_data_accessors *self.identities.flatten

  self.associations.each do |name,config|
    self.associations[name] = config.to_hash
  end

  self.define_associations

  super
end

.all(condition = {}) ⇒ Array<Model>

Looks up in the identity map first.

Parameters:

  • condition (Hash) (defaults to: {})

    ?

Returns:

  • (Array<Model>)

    all matching records



379
380
381
# File 'lib/praxis-mapper/model.rb', line 379

def self.all(condition={})
  IdentityMap.current.all(self, condition)
end

.array_to_many(name, &block) ⇒ Object

Define array_to_many (aka: belongs_to where the key attribute is an array)



139
140
141
# File 'lib/praxis-mapper/model.rb', line 139

def self.array_to_many(name, &block)
  self.associations[name] = ConfigHash.from(type: :array_to_many, &block)
end

.belongs_to(name = nil, belongs_to_options = {}) ⇒ Array

Implements Praxis::Mapper DSL directive ‘belongs_to’. If name and belongs_to_options are given, upserts the association. If only name is given, gets the named association. Else, returns all configured associations.

Examples:

belongs_to :parents, :model => ParentModel,
    :source_key => :parent_ids,
    :fk => :id,
    :type => :array

Parameters:

  • name (String) (defaults to: nil)

    name of association to set or get

  • belongs_to_options (Hash) (defaults to: {})

    new association options

  • :model (Hash)

    a customizable set of options

  • :fk (Hash)

    a customizable set of options

  • :source_key (Hash)

    a customizable set of options

  • :type (Hash)

    a customizable set of options

Returns:

  • (Array)

    all configured associations



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/praxis-mapper/model.rb', line 102

def self.belongs_to(name=nil, belongs_to_options={})
  if !belongs_to_options.empty?
    warn "DEPRECATION: `#{self}.belongs_to` is deprecated. Use `many_to_one` or `array_to_many` instead."

    opts = {:fk => :id}.merge(belongs_to_options)

    opts[:key] = opts.delete(:source_key)
    opts[:primary_key] = opts.delete(:fk) if opts.has_key?(:fk)

    if (opts.delete(:type) == :array)
      opts[:type] = :array_to_many
    else
      opts[:type] = :many_to_one
    end

    self.associations[name] = opts


    define_belongs_to(name, opts)
  else
    raise "Calling Model.belongs to fetch association information is no longer supported. Use Model.associations instead."
  end
end

.context(name, &block) ⇒ Object



215
216
217
218
219
220
# File 'lib/praxis-mapper/model.rb', line 215

def self.context(name, &block)
  default = Hash.new do |hash, key|
    hash[key] = Array.new
  end
  @contexts[name] = ConfigHash.from(default, &block).to_hash
end

.define_array_to_many(name, association) ⇒ Object



299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/praxis-mapper/model.rb', line 299

def self.define_array_to_many(name, association)
  model = association[:model]
  primary_key = association.fetch(:primary_key, :id)
  key = association.fetch(:key)
  
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
   def #{name}
      return nil if #{key}.nil?
      @__#{name} ||= self.identity_map.all(#{model.name},#{primary_key.inspect} => #{key})
    end
  RUBY

end

.define_association(name, association) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/praxis-mapper/model.rb', line 246

def self.define_association(name, association)
  case association[:type]
  when :many_to_one
    self.define_many_to_one(name, association)
  when :array_to_many
    self.define_array_to_many(name, association)
  when :one_to_many
    self.define_one_to_many(name, association)
  when :many_to_array
    self.define_many_to_array(name, association)
  end
end

.define_associationsObject



240
241
242
243
244
# File 'lib/praxis-mapper/model.rb', line 240

def self.define_associations
  self.associations.each do |name, association|
    self.define_association(name,association)
  end
end

.define_belongs_to(name, opts) ⇒ Object

The belongs_to association creates a one-to-one match with another model. In database terms, this association says that this class contains the foreign key.

Examples:

define_belongs_to(:customer, {:model => Customer, :fk => :id, :source_key => :customer_id, :type => scalar})

Parameters:

  • name (Symbol)

    name of association; typically the same as associated model name

  • opts (Hash)

    association options

  • :model (Hash)

    a customizable set of options

  • :fk (Hash)

    a customizable set of options

  • :source_key (Hash)

    a customizable set of options

  • :type (Hash)

    a customizable set of options

See Also:



353
354
355
356
357
358
359
360
361
362
363
# File 'lib/praxis-mapper/model.rb', line 353

def self.define_belongs_to(name, opts)
  model = opts.fetch(:model)
  type = opts.fetch(:type, :many_to_one) # :scalar has no meaning other than it's not an array

  case opts.fetch(:type, :many_to_one)      
  when :many_to_one
    return self.define_many_to_one(name, opts)
  when :array_to_many
    return self.define_array_to_many(name, opts)
  end
end

.define_data_accessor(name) ⇒ Object



230
231
232
233
234
235
236
237
238
# File 'lib/praxis-mapper/model.rb', line 230

def self.define_data_accessor(name)
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{name}
      @__#{name} ||= @data.fetch(#{name.inspect}) do
        raise "field #{name.inspect} not loaded for #{self.inspect}."
      end.freeze
    end
  RUBY
end

.define_data_accessors(*names) ⇒ Object



223
224
225
226
227
# File 'lib/praxis-mapper/model.rb', line 223

def self.define_data_accessors(*names)
  names.each do |name|
    self.define_data_accessor(name)
  end
end

.define_many_to_array(name, association) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/praxis-mapper/model.rb', line 314

def self.define_many_to_array(name, association)
  model = association[:model]
  primary_key = association.fetch(:primary_key, :id)
  key_name = association.fetch(:key)

  if primary_key.kind_of?(Array)
    key = "["
    key += primary_key.collect { |k| "self.#{k}" }.join(", ")
    key += "]"
  else
    key = "self.#{primary_key}"
  end

  module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{name}
      key = #{key}
      return nil if key.nil?
      @__#{name} ||= self.identity_map.all(#{model.name}).
        select { |record| record.#{key_name}.include? key }
    end
  RUBY
end

.define_many_to_one(name, association) ⇒ Object



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/praxis-mapper/model.rb', line 277

def self.define_many_to_one(name, association)
  model = association[:model]
  primary_key = association.fetch(:primary_key, :id)

  if association[:key].kind_of?(Array)
    key = "["
    key += association[:key].collect { |k| "self.#{k}" }.join(", ")
    key += "]"
  else
    key = "self.#{association[:key]}"
  end

  module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{name}
      return nil if #{key}.nil?
      @__#{name} ||= self.identity_map.get(#{model.name},#{primary_key.inspect} => #{key})
    end
  RUBY

end

.define_one_to_many(name, association) ⇒ Object

has_many



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/praxis-mapper/model.rb', line 260

def self.define_one_to_many(name, association)
  model = association[:model]
  primary_key = association.fetch(:primary_key, :id)
  
  if primary_key.kind_of?(Array)
    define_method(name) do 
      pk = primary_key.collect { |k| self.send(k) }
      self.identity_map.all(model,association[:key] => [pk])
    end
  else
    define_method(name) do
      pk = self.send(primary_key)
      self.identity_map.all(model,association[:key] => [pk])
    end
  end
end

.define_serialized_accessor(name, serializer, **opts) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/praxis-mapper/model.rb', line 201

def self.define_serialized_accessor(name, serializer, **opts)
  define_method(name) do
    @deserialized_data[name] ||= if (value = @data.fetch(name))
      serializer.load(value)
    else
      opts[:default]
    end
  end

  define_method("_raw_#{name}".to_sym) do
    @data.fetch name
  end
end

.excluded_scopes(*scopes) ⇒ Array

Implements Praxis::Mapper DSL directive ‘excluded_scopes’. Gets or sets the excluded scopes for this model. Exclusion means that the named condition cannot be applied.

Examples:

excluded_scopes :account, :deleted_at

Parameters:

  • *scopes (Array)

    list of scopes to exclude

Returns:

  • (Array)

    configured list of scopes



50
51
52
53
54
55
56
# File 'lib/praxis-mapper/model.rb', line 50

def self.excluded_scopes(*scopes)
  if scopes.any?
    self.config[:excluded_scopes] = scopes
  else
    self.config.fetch(:excluded_scopes)
  end
end

.get(condition) ⇒ Model

Looks up in the identity map first.

Parameters:

  • condition

    ?

Returns:

  • (Model)

    matching record



370
371
372
# File 'lib/praxis-mapper/model.rb', line 370

def self.get(condition)
  IdentityMap.current.get(self, condition)
end

.identities(*names) ⇒ Array

Implements Praxis::Mapper DSL directive ‘identities’. Gets or sets list of identity fields.

Examples:

identities :id, :type

Parameters:

  • *names (Array)

    list of identity fields to set

Returns:

  • (Array)

    configured list of identity fields



164
165
166
167
168
169
170
171
# File 'lib/praxis-mapper/model.rb', line 164

def self.identities(*names)
  if names.any?
    self.config[:identities] = names
    @_identities = names
  else
    self.config.fetch(:identities)
  end
end

.identity(name) ⇒ Object

Adds given identity to the list of model identities. May be an array for composite keys.



151
152
153
154
155
# File 'lib/praxis-mapper/model.rb', line 151

def self.identity(name)
  @_identities ||= Array.new
  @_identities << name
  self.config[:identities] << name
end

.inherited(klass) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/praxis-mapper/model.rb', line 16

def self.inherited(klass)
  super

  klass.instance_eval do
    @config = {
      excluded_scopes: [],
      identities: []
    }
    @associations = {}
    @serialized_fields = {}
    @contexts = Hash.new
  end
end

.json(name, opts = {}) ⇒ Object

Implements Praxis::Mapper DSL directive ‘json’. This will perform JSON.load on serialized data.

Examples:

yaml :parent_ids, :default => []

Parameters:

  • name (String)

    name of field that is serialized as JSON

  • opts (Hash) (defaults to: {})


196
197
198
199
# File 'lib/praxis-mapper/model.rb', line 196

def self.json(name, opts={})
  @serialized_fields[name] = :json
  define_serialized_accessor(name, JSON, opts)
end

.many_to_array(name, &block) ⇒ Object

Define many_to_array (aka: has_many where the key attribute is an array)



144
145
146
# File 'lib/praxis-mapper/model.rb', line 144

def self.many_to_array(name, &block)
  self.associations[name] = ConfigHash.from(type: :many_to_array, &block)
end

.many_to_one(name, &block) ⇒ Object

Define many_to_one (aka: belongs_to)



134
135
136
# File 'lib/praxis-mapper/model.rb', line 134

def self.many_to_one(name, &block)
  self.associations[name] = ConfigHash.from(type: :many_to_one, &block)
end

.one_to_many(name, &block) ⇒ Object

Define one_to_many (aka: has_many)



128
129
130
# File 'lib/praxis-mapper/model.rb', line 128

def self.one_to_many(name, &block)
  self.associations[name] = ConfigHash.from(type: :one_to_many, &block)
end

.repository_name(name = nil) ⇒ Symbol

Gets or sets the repository for this model.

Parameters:

  • name (Symbol) (defaults to: nil)

    repository name

Returns:

  • (Symbol)

    repository name or :default



62
63
64
65
66
67
68
# File 'lib/praxis-mapper/model.rb', line 62

def self.repository_name(name=nil)
  if name
    self.config[:repository_name] = name
  else
    self.config.fetch(:repository_name, :default)
  end
end

.table_name(name = nil) ⇒ Symbol

Implements Praxis::Mapper DSL directive ‘table_name’. Gets or sets the SQL-like table name. Can also be thought of as a namespace in the repository.

Examples:

table_name ‘json_array_model’

Parameters:

  • name (Symbol) (defaults to: nil)

    table name

Returns:

  • (Symbol)

    table name or nil



77
78
79
80
81
82
83
# File 'lib/praxis-mapper/model.rb', line 77

def self.table_name(name=nil)
  if name
    self.config[:table_name] = name
  else
    self.config.fetch(:table_name, nil)
  end
end

.yaml(name, opts = {}) ⇒ Object

Implements Praxis::Mapper DSL directive ‘yaml’. This will perform YAML.load on serialized data.

Examples:

yaml :parent_ids, :default => []

Parameters:

  • name (String)

    name of field that is serialized as YAML

  • opts (Hash) (defaults to: {})


182
183
184
185
# File 'lib/praxis-mapper/model.rb', line 182

def self.yaml(name, opts={})
  @serialized_fields[name] = :yaml
  define_serialized_accessor(name, YAML, opts)
end

Instance Method Details

#_dataObject



425
426
427
# File 'lib/praxis-mapper/model.rb', line 425

def _data
  @data
end

#identitiesObject



414
415
416
417
418
419
420
421
422
423
# File 'lib/praxis-mapper/model.rb', line 414

def identities
  self.class._identities.each_with_object(Hash.new) do |identity, hash|
    case identity
    when Symbol
      hash[identity] = @data[identity].freeze
    else
      hash[identity] = @data.values_at(*identity).collect(&:freeze)
    end
  end
end

#inspectObject



391
392
393
394
395
396
397
# File 'lib/praxis-mapper/model.rb', line 391

def inspect
"#<#{self.class}:0x#{object_id.to_s(16)} #{
      instance_variables.select{|v| InspectedFields.include? v}.map {|var|
        "#{var}: #{instance_variable_get(var).inspect}"
      }.join("#{'  '}")
    }#{'  '}>"
end

#original_method_missingObject



14
15
16
17
18
19
20
21
# File 'lib/praxis-mapper/support/factory_bot.rb', line 14

def method_missing(name, *args)
  if @data.has_key? name
    self.class.define_data_accessor(name)
    self.send(name)
  else
    super
  end
end

#respond_to_missing?(name) ⇒ Boolean

Returns:

  • (Boolean)


399
400
401
# File 'lib/praxis-mapper/model.rb', line 399

def respond_to_missing?(name, *)
  @data.key?(name) || super
end

#save!Object



7
8
9
10
11
12
# File 'lib/praxis-mapper/support/factory_bot.rb', line 7

def save!
  @new_record = true
  unless Praxis::Mapper::IdentityMap.current.add_records([self]).include? self
    raise "Conflict trying to save record with type: #{self.class} and data:\n#{@data.pretty_inspect}"
  end
end

#set_association(name, value) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/praxis-mapper/support/factory_bot.rb', line 50

def set_association(name, value)
  spec = self.class.associations.fetch(name)

  case spec[:type]
  when :one_to_many
    raise "can not set one_to_many associations to nil" if value.nil?
    primary_key = @data[spec[:primary_key]]
    setter_name = "#{spec[:key]}="
    Array(value).each { |item| item.send(setter_name, primary_key) }
  when :many_to_one
    primary_key = value && value.send(spec[:primary_key])
    @data[spec[:key]] = primary_key
  else
    raise "can not handle associations of type #{spec[:type]}"
  end

end

#set_serialized_field(name, value) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/praxis-mapper/support/factory_bot.rb', line 37

def set_serialized_field(name,value)
  @deserialized_data[name] = value

  case self.class.serialized_fields[name]
  when :json
    @data[name] = JSON.dump(value)
  when :yaml
    @data[name] = YAML.dump(value)
  else
    @data[name] = value # dunno
  end
end