Module: Diametric::Entity::ClassMethods

Defined in:
lib/diametric/entity.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#attributesArray (readonly)

Returns Definitions of each of the entity's attributes (name, type, options).

Returns:

  • (Array)

    Definitions of each of the entity's attributes (name, type, options).


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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/diametric/entity.rb', line 86

module ClassMethods
  def partition
    @partition
  end

  def partition=(partition)
    self.partition = partition.to_sym
  end

  def attributes
    @attributes
  end

  def defaults
    @defaults
  end

  # Set the namespace prefix used for attribute names
  #
  # @param prefix [#to_s] The prefix to be used for namespacing entity attributes
  #
  # @example Override the default namespace prefix
  #   class Mouse
  #     include Diametric::Entity
  #
  #     namespace_prefix :mice
  #   end
  #
  #   Mouse.new.prefix # => :mice
  #
  # @return void
  def namespace_prefix(prefix)
    @namespace_prefix = prefix
  end

  # Add an attribute to a {Diametric::Entity}.
  #
  # Valid options are:
  #
  # * +:index+: The only valid value is +true+. This causes the
  #   attribute to be indexed for easier lookup.
  # * +:unique+: Valid values are +:value+ or +:identity.+
  #   * +:value+ causes the attribute value to be unique to the
  #     entity and attempts to insert a duplicate value will fail.
  #   * +:identity+ causes the attribute value to be unique to
  #     the entity. Attempts to insert a duplicate value with a
  #     temporary entity id will result in an "upsert," causing the
  #     temporary entity's attributes to be merged with those for
  #     the current entity in Datomic.
  # * +:cardinality+: Specifies whether an attribute associates
  #   a single value or a set of values with an entity. The
  #   values allowed are:
  #   * +:one+ - the attribute is single valued, it associates a
  #     single value with an entity.
  #   * +:many+ - the attribute is mutli valued, it associates a
  #     set of values with an entity.
  #   +:one+ is the default.
  # * +:doc+: A string used in Datomic to document the attribute.
  # * +:fulltext+: The only valid value is +true+. Indicates that a
  #   fulltext search index should be generated for the attribute.
  # * +:default+: The value the attribute will default to when the
  #   Entity is initialized. Defaults for attributes with +:cardinality+ of +:many+
  #   will be transformed into a Set by passing the default to +Set.new+.
  #
  # @example Add an indexed name attribute.
  #   attribute :name, String, :index => true
  #
  # @param name [String] The attribute's name.
  # @param value_type [Class] The attribute's type.
  #   Must exist in {Diametric::Entity::VALUE_TYPES}.
  # @param opts [Hash] Options to pass to Datomic.
  #
  # @return void
  def attribute(name, value_type, opts = {})
    opts = DEFAULT_OPTIONS.merge(opts)

    establish_defaults(name, value_type, opts)

    @attributes[name] = {:value_type => value_type}.merge(opts)

    setup_attribute_methods(name, opts[:cardinality])
  end

  # @return [Array<Symbol>] Names of the entity's attributes.
  def attribute_names
    @attributes.keys
  end

  # @return [Array<Symbol>] Names of the entity's enums.
  def enum_names
    @enums.keys
  end

  # Add an enum to a {Diametric::Entity}.
  #
  # enum is used when attribute type is Ref and refers
  # a set of values.
  # name should be the same as corresponding attribute name.
  #
  # @example Add an enum of colors
  #   class Palette
  #     attribute :color, Ref
  #     enum :color, [:blue, :green, :yellow, :orange]
  #   end
  #   p = Pallet.new
  #   p.color = Pallet::Color::Green
  #
  # @param name [Symbol] The enum's name.
  # @param values [Array] The enum values.
  #
  # @return void
  def enum(name, values)
    enum_values = nil
    enum_values = values.to_set if values.is_a?(Array)
    enum_values = values if values.is_a?(Set)
    raise RuntimeError "values should be Array or Set" if enum_values.nil?
    enum_name = name.to_s.capitalize
    syms = values.collect(&:to_s).collect(&:upcase).collect(&:to_sym)
    class_eval("module #{enum_name};enum #{syms};end")
    @enums[name] = syms
  end

  # Generates a Datomic schema for a model's attributes.
  #
  # @return [Array] A Datomic schema, as Ruby data that can be
  #   converted to EDN.
  def schema
    defaults = {
      :"db/cardinality" => :"db.cardinality/one",
      :"db.install/_attribute" => :"db.part/db"
    }

    schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
      opts = opts.dup
      value_type = opts.delete(:value_type)

      unless opts.empty?
        opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
        opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
        opts = opts.map { |k, v|
          k = namespace("db", k)
          [k, v]
        }
        opts = Hash[*opts.flatten]
      end

      schema << defaults.merge({
                                 :"db/ident" => namespace(prefix, attribute),
                                 :"db/valueType" => value_type(value_type),
                                 :"db/id" => tempid(:"db.part/db")
                               }).merge(opts)
    end
    enum_schema = [
      :"db/add", tempid(:"db.part/user"), :"db/ident"
    ]
    prefix = self.to_s.underscore
    @enums.each do |key, values|
      values.each do |value|
        ident_value = :"#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
        es = [:"db/add", tempid(:"db.part/user"), :"db/ident", ident_value]
        schema_array << es
      end
    end
    schema_array
  end

  # Given a set of Ruby data returned from a Datomic query, this
  # can re-hydrate that data into a model instance.
  #
  # @return [Entity]
  def from_query(query_results, connection=nil, resolve=false)
    dbid = query_results.shift
    widget = self.new(Hash[attribute_names.zip query_results])
    widget.dbid = dbid

    if resolve
      widget = resolve_ref_dbid(widget, connection)
    end
    widget
  end

  def resolve_ref_dbid(parent, connection)
    parent.class.attribute_names.each do |e|
      if parent.class.attributes[e][:value_type] == "ref"
        ref = parent.instance_variable_get("@#{e.to_s}")
        if ref.is_a?(Fixnum) ||
          (self.instance_variable_get("@peer") && ref.is_a?(Diametric::Persistence::Entity))
          child = reify(ref, connection)
          child = resolve_ref_dbid(child, connection)
          parent.instance_variable_set("@#{e.to_s}", child)
        elsif ref.is_a?(Diametric::Associations::Collection)
          children = Diametric::Associations::Collection.new(parent, e.to_s)
          ref.each do |entity|
            children.add_reified_entities(reify(entity, connection))
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        elsif ref.is_a?(Set)
          children = ref.inject(Set.new) do |memo, entity|
            child = reify(entity, connection)
            memo.add(child)
            memo
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        end
      end
    end
    parent
  end

  def reify(thing, conn_or_db=nil, resolve=false)
    return peer_reify(thing, conn_or_db, resolve) if self.instance_variable_get("@peer")
    rest_reify(thing, resolve)
  end

  def rest_reify(dbid, resolve)
    query = [
      :find, ~"?ident", ~"?v",
      :in, ~"\$", [~"?e"],
      :where, [~"?e", ~"?a", ~"?v"], [~"?a", :"db/ident", ~"?ident"]
    ]
    entities = self.q(query, [[dbid]])
    class_name = to_classname(entities.first.first)
    instance = eval("#{class_name}.new")
    entities.each do |k, v|
      matched_data = /([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(k.to_s)
      attribute = instance.send(matched_data[2])
      if attribute && attribute.is_a?(Diametric::Associations::Collection)
        attribute.add_reified_entities(v)
      else
        instance.send("clean_#{matched_data[2]}=", v)
      end
    end
    instance.send("dbid=", dbid)

    if resolve
      instance = resolve_ref_dbid(instance, nil)
    end

    instance
  end

  def peer_reify(thing, conn_or_db=nil, resolve=false)
    conn_or_db ||= Diametric::Persistence::Peer.connect.db

    if conn_or_db.respond_to?(:db)
      conn_or_db = conn_or_db.db
    end

    if thing.is_a? Fixnum
      dbid = thing
      entity = conn_or_db.entity(dbid)
    elsif thing.respond_to?(:eid)
      dbid = thing.eid
      if entity.respond_to?(:keys)
        entity = thing
      else
        entity = conn_or_db.entity(dbid)
      end
    elsif thing.kind_of? java.lang.Long
      entity = conn_or_db.entity(Diametric::Persistence::Object.new(thing))
    elsif thing.respond_to?(:to_java)
      dbid = thing.to_java
      entity = conn_or_db.entity(dbid)
    else
      return thing
    end
    first_key = entity.keys.first
    class_name = to_classname(first_key)
    instance = eval("#{class_name}.new")
    entity.keys.each do |key|
      matched_data = /:([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(key)
      instance.send("clean_#{matched_data[2]}=", entity[key])
    end
    instance.send("dbid=", Diametric::Persistence::Object.new(entity.get("db/id")))

    if resolve
      instance = resolve_ref_dbid(instance, conn_or_db)
    end

    instance
  end

  def to_classname(key)
    names = []
    # drops the first character ":"
    key = key.to_s
    if key[0] == ":"
      key = key[1..-1]
    end
    key.chars.inject("") do |memo, c|
      if c == "/"
        # means the end of class name
        names << memo
        break
      elsif c == "."
        # namespace delimiter
        names << memo
        memo = ""
      elsif c == "-"
        # Clojure uses - for name, but Ruby can't
        # converts dash to underscore
        memo << "_"
      else
        memo << c
      end
      memo
    end
    names.collect(&:camelize).join("::")
  end

  def find(id)
    if self.instance_variable_get("@peer")
      connection ||= Diametric::Persistence::Peer.connect
    end
    reify(id, connection)
  end

  # Create a temporary id placeholder.
  #
  # @param e [*#to_edn] Elements to put in the placeholder. Should
  #   be either partition or partition and a negative number to be
  #   used as a reference.
  #
  # @return [EDN::Type::Unknown] Temporary id placeholder.
  def tempid(*e)
    if self.instance_variable_get("@peer")
      Diametric::Persistence::Peer.tempid(*e)
    else
      Diametric::Persistence::REST.tempid(*e)
    end
  end

  # Returns the prefix for this model used in Datomic. Can be
  # overriden by declaring {#namespace_prefix}
  #
  # @example
  #   Mouse.prefix #=> "mouse"
  #   FireHouse.prefix #=> "fire_house"
  #   Person::User.prefix #=> "person.user"
  #
  # @return [String]
  def prefix
    @namespace_prefix || self.to_s.underscore.gsub('/', '.')
  end

  # Namespace a attribute for Datomic.
  #
  # @param ns [#to_s] Namespace.
  # @param attribute [#to_s] Attribute.
  #
  # @return [Symbol] Namespaced attribute.
  def namespace(ns, attribute)
    [ns.to_s, attribute.to_s].join("/").to_sym
  end

  # Raise an error if validation failed.
  #
  # @example Raise the validation error.
  #   Person.fail_validate!(person)
  #
  # @param [ Entity ] entity The entity to fail.
  def fail_validate!(entity)
    raise Errors::ValidationError.new(entity)
  end

  private

  def value_type(vt)
    if vt.is_a?(Class) || vt.is_a?(Module)
      vt = VALUE_TYPES[vt]
    end
    namespace("db.type", vt)
  end

  def establish_defaults(name, value_type, opts = {})
    default = opts.delete(:default)
    @defaults[name] = default if default
  end

  def setup_attribute_methods(name, cardinality)
    define_attribute_method name

    define_method(name) do
      ivar = instance_variable_get("@#{name}")
      if ivar.nil? &&
          self.class.attributes[name][:value_type] == Ref &&
          self.class.attributes[name][:cardinality] == :many
        ivar = Diametric::Associations::Collection.new(self, name)
      end
      ivar
    end

    define_method("#{name}=") do |value|
      send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
      case self.class.attributes[name][:value_type]
      when Ref
        case cardinality
        when :many
          if value.is_a?(Enumerable) && value.first.respond_to?(:save)
            # entity type
            ivar = send("#{name}")
            instance_variable_set("@#{name}", ivar.replace(value))
          elsif value.is_a?(Diametric::Associations::Collection)
            instance_variable_set("@#{name}", value)
          elsif value.is_a?(Enumerable)
            # enum type
            # however, falls here when empty array is given for entity type
            instance_variable_set("@#{name}", Set.new(value))
          end
        when :one
          if value.respond_to?(:save)
            # entity type
            if value.save
              instance_variable_set("@#{name}", value.dbid)
            end
          else
            # enum type
            instance_variable_set("@#{name}", value)
          end
        end
      else   # not Ref
        case cardinality
        when :many
          instance_variable_set("@#{name}", Set.new(value))
        when :one
          instance_variable_set("@#{name}", value)
        end
      end
    end

    define_method("clean_#{name}=") do |value|
      if self.class.attributes[name][:value_type] != Ref && cardinality == :many
        if value.is_a? Enumerable
          value = Set.new(value)
        else
          # used from rest reify
          ivar = instance_variable_get("@#{name}")
          ivar ||= Set.new
          value = ivar.add(value)
        end
      end
      if self.class.attributes[name][:value_type] == Ref &&
          cardinality == :many &&
          !(value.is_a? Diametric::Associations::Collection)
        if value.is_a? Enumerable
          value = Diametric::Associations::Collection.new(self, name, value)
        else
          value = Diametric::Associations::Collection.new(self, name, [value])
        end
      end
      instance_variable_set("@#{name}", value)
    end
  end
end

#defaultsArray (readonly)

Returns Default values for any entitites defined with a :default key and value.

Returns:

  • (Array)

    Default values for any entitites defined with a :default key and value.


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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/diametric/entity.rb', line 86

module ClassMethods
  def partition
    @partition
  end

  def partition=(partition)
    self.partition = partition.to_sym
  end

  def attributes
    @attributes
  end

  def defaults
    @defaults
  end

  # Set the namespace prefix used for attribute names
  #
  # @param prefix [#to_s] The prefix to be used for namespacing entity attributes
  #
  # @example Override the default namespace prefix
  #   class Mouse
  #     include Diametric::Entity
  #
  #     namespace_prefix :mice
  #   end
  #
  #   Mouse.new.prefix # => :mice
  #
  # @return void
  def namespace_prefix(prefix)
    @namespace_prefix = prefix
  end

  # Add an attribute to a {Diametric::Entity}.
  #
  # Valid options are:
  #
  # * +:index+: The only valid value is +true+. This causes the
  #   attribute to be indexed for easier lookup.
  # * +:unique+: Valid values are +:value+ or +:identity.+
  #   * +:value+ causes the attribute value to be unique to the
  #     entity and attempts to insert a duplicate value will fail.
  #   * +:identity+ causes the attribute value to be unique to
  #     the entity. Attempts to insert a duplicate value with a
  #     temporary entity id will result in an "upsert," causing the
  #     temporary entity's attributes to be merged with those for
  #     the current entity in Datomic.
  # * +:cardinality+: Specifies whether an attribute associates
  #   a single value or a set of values with an entity. The
  #   values allowed are:
  #   * +:one+ - the attribute is single valued, it associates a
  #     single value with an entity.
  #   * +:many+ - the attribute is mutli valued, it associates a
  #     set of values with an entity.
  #   +:one+ is the default.
  # * +:doc+: A string used in Datomic to document the attribute.
  # * +:fulltext+: The only valid value is +true+. Indicates that a
  #   fulltext search index should be generated for the attribute.
  # * +:default+: The value the attribute will default to when the
  #   Entity is initialized. Defaults for attributes with +:cardinality+ of +:many+
  #   will be transformed into a Set by passing the default to +Set.new+.
  #
  # @example Add an indexed name attribute.
  #   attribute :name, String, :index => true
  #
  # @param name [String] The attribute's name.
  # @param value_type [Class] The attribute's type.
  #   Must exist in {Diametric::Entity::VALUE_TYPES}.
  # @param opts [Hash] Options to pass to Datomic.
  #
  # @return void
  def attribute(name, value_type, opts = {})
    opts = DEFAULT_OPTIONS.merge(opts)

    establish_defaults(name, value_type, opts)

    @attributes[name] = {:value_type => value_type}.merge(opts)

    setup_attribute_methods(name, opts[:cardinality])
  end

  # @return [Array<Symbol>] Names of the entity's attributes.
  def attribute_names
    @attributes.keys
  end

  # @return [Array<Symbol>] Names of the entity's enums.
  def enum_names
    @enums.keys
  end

  # Add an enum to a {Diametric::Entity}.
  #
  # enum is used when attribute type is Ref and refers
  # a set of values.
  # name should be the same as corresponding attribute name.
  #
  # @example Add an enum of colors
  #   class Palette
  #     attribute :color, Ref
  #     enum :color, [:blue, :green, :yellow, :orange]
  #   end
  #   p = Pallet.new
  #   p.color = Pallet::Color::Green
  #
  # @param name [Symbol] The enum's name.
  # @param values [Array] The enum values.
  #
  # @return void
  def enum(name, values)
    enum_values = nil
    enum_values = values.to_set if values.is_a?(Array)
    enum_values = values if values.is_a?(Set)
    raise RuntimeError "values should be Array or Set" if enum_values.nil?
    enum_name = name.to_s.capitalize
    syms = values.collect(&:to_s).collect(&:upcase).collect(&:to_sym)
    class_eval("module #{enum_name};enum #{syms};end")
    @enums[name] = syms
  end

  # Generates a Datomic schema for a model's attributes.
  #
  # @return [Array] A Datomic schema, as Ruby data that can be
  #   converted to EDN.
  def schema
    defaults = {
      :"db/cardinality" => :"db.cardinality/one",
      :"db.install/_attribute" => :"db.part/db"
    }

    schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
      opts = opts.dup
      value_type = opts.delete(:value_type)

      unless opts.empty?
        opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
        opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
        opts = opts.map { |k, v|
          k = namespace("db", k)
          [k, v]
        }
        opts = Hash[*opts.flatten]
      end

      schema << defaults.merge({
                                 :"db/ident" => namespace(prefix, attribute),
                                 :"db/valueType" => value_type(value_type),
                                 :"db/id" => tempid(:"db.part/db")
                               }).merge(opts)
    end
    enum_schema = [
      :"db/add", tempid(:"db.part/user"), :"db/ident"
    ]
    prefix = self.to_s.underscore
    @enums.each do |key, values|
      values.each do |value|
        ident_value = :"#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
        es = [:"db/add", tempid(:"db.part/user"), :"db/ident", ident_value]
        schema_array << es
      end
    end
    schema_array
  end

  # Given a set of Ruby data returned from a Datomic query, this
  # can re-hydrate that data into a model instance.
  #
  # @return [Entity]
  def from_query(query_results, connection=nil, resolve=false)
    dbid = query_results.shift
    widget = self.new(Hash[attribute_names.zip query_results])
    widget.dbid = dbid

    if resolve
      widget = resolve_ref_dbid(widget, connection)
    end
    widget
  end

  def resolve_ref_dbid(parent, connection)
    parent.class.attribute_names.each do |e|
      if parent.class.attributes[e][:value_type] == "ref"
        ref = parent.instance_variable_get("@#{e.to_s}")
        if ref.is_a?(Fixnum) ||
          (self.instance_variable_get("@peer") && ref.is_a?(Diametric::Persistence::Entity))
          child = reify(ref, connection)
          child = resolve_ref_dbid(child, connection)
          parent.instance_variable_set("@#{e.to_s}", child)
        elsif ref.is_a?(Diametric::Associations::Collection)
          children = Diametric::Associations::Collection.new(parent, e.to_s)
          ref.each do |entity|
            children.add_reified_entities(reify(entity, connection))
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        elsif ref.is_a?(Set)
          children = ref.inject(Set.new) do |memo, entity|
            child = reify(entity, connection)
            memo.add(child)
            memo
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        end
      end
    end
    parent
  end

  def reify(thing, conn_or_db=nil, resolve=false)
    return peer_reify(thing, conn_or_db, resolve) if self.instance_variable_get("@peer")
    rest_reify(thing, resolve)
  end

  def rest_reify(dbid, resolve)
    query = [
      :find, ~"?ident", ~"?v",
      :in, ~"\$", [~"?e"],
      :where, [~"?e", ~"?a", ~"?v"], [~"?a", :"db/ident", ~"?ident"]
    ]
    entities = self.q(query, [[dbid]])
    class_name = to_classname(entities.first.first)
    instance = eval("#{class_name}.new")
    entities.each do |k, v|
      matched_data = /([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(k.to_s)
      attribute = instance.send(matched_data[2])
      if attribute && attribute.is_a?(Diametric::Associations::Collection)
        attribute.add_reified_entities(v)
      else
        instance.send("clean_#{matched_data[2]}=", v)
      end
    end
    instance.send("dbid=", dbid)

    if resolve
      instance = resolve_ref_dbid(instance, nil)
    end

    instance
  end

  def peer_reify(thing, conn_or_db=nil, resolve=false)
    conn_or_db ||= Diametric::Persistence::Peer.connect.db

    if conn_or_db.respond_to?(:db)
      conn_or_db = conn_or_db.db
    end

    if thing.is_a? Fixnum
      dbid = thing
      entity = conn_or_db.entity(dbid)
    elsif thing.respond_to?(:eid)
      dbid = thing.eid
      if entity.respond_to?(:keys)
        entity = thing
      else
        entity = conn_or_db.entity(dbid)
      end
    elsif thing.kind_of? java.lang.Long
      entity = conn_or_db.entity(Diametric::Persistence::Object.new(thing))
    elsif thing.respond_to?(:to_java)
      dbid = thing.to_java
      entity = conn_or_db.entity(dbid)
    else
      return thing
    end
    first_key = entity.keys.first
    class_name = to_classname(first_key)
    instance = eval("#{class_name}.new")
    entity.keys.each do |key|
      matched_data = /:([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(key)
      instance.send("clean_#{matched_data[2]}=", entity[key])
    end
    instance.send("dbid=", Diametric::Persistence::Object.new(entity.get("db/id")))

    if resolve
      instance = resolve_ref_dbid(instance, conn_or_db)
    end

    instance
  end

  def to_classname(key)
    names = []
    # drops the first character ":"
    key = key.to_s
    if key[0] == ":"
      key = key[1..-1]
    end
    key.chars.inject("") do |memo, c|
      if c == "/"
        # means the end of class name
        names << memo
        break
      elsif c == "."
        # namespace delimiter
        names << memo
        memo = ""
      elsif c == "-"
        # Clojure uses - for name, but Ruby can't
        # converts dash to underscore
        memo << "_"
      else
        memo << c
      end
      memo
    end
    names.collect(&:camelize).join("::")
  end

  def find(id)
    if self.instance_variable_get("@peer")
      connection ||= Diametric::Persistence::Peer.connect
    end
    reify(id, connection)
  end

  # Create a temporary id placeholder.
  #
  # @param e [*#to_edn] Elements to put in the placeholder. Should
  #   be either partition or partition and a negative number to be
  #   used as a reference.
  #
  # @return [EDN::Type::Unknown] Temporary id placeholder.
  def tempid(*e)
    if self.instance_variable_get("@peer")
      Diametric::Persistence::Peer.tempid(*e)
    else
      Diametric::Persistence::REST.tempid(*e)
    end
  end

  # Returns the prefix for this model used in Datomic. Can be
  # overriden by declaring {#namespace_prefix}
  #
  # @example
  #   Mouse.prefix #=> "mouse"
  #   FireHouse.prefix #=> "fire_house"
  #   Person::User.prefix #=> "person.user"
  #
  # @return [String]
  def prefix
    @namespace_prefix || self.to_s.underscore.gsub('/', '.')
  end

  # Namespace a attribute for Datomic.
  #
  # @param ns [#to_s] Namespace.
  # @param attribute [#to_s] Attribute.
  #
  # @return [Symbol] Namespaced attribute.
  def namespace(ns, attribute)
    [ns.to_s, attribute.to_s].join("/").to_sym
  end

  # Raise an error if validation failed.
  #
  # @example Raise the validation error.
  #   Person.fail_validate!(person)
  #
  # @param [ Entity ] entity The entity to fail.
  def fail_validate!(entity)
    raise Errors::ValidationError.new(entity)
  end

  private

  def value_type(vt)
    if vt.is_a?(Class) || vt.is_a?(Module)
      vt = VALUE_TYPES[vt]
    end
    namespace("db.type", vt)
  end

  def establish_defaults(name, value_type, opts = {})
    default = opts.delete(:default)
    @defaults[name] = default if default
  end

  def setup_attribute_methods(name, cardinality)
    define_attribute_method name

    define_method(name) do
      ivar = instance_variable_get("@#{name}")
      if ivar.nil? &&
          self.class.attributes[name][:value_type] == Ref &&
          self.class.attributes[name][:cardinality] == :many
        ivar = Diametric::Associations::Collection.new(self, name)
      end
      ivar
    end

    define_method("#{name}=") do |value|
      send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
      case self.class.attributes[name][:value_type]
      when Ref
        case cardinality
        when :many
          if value.is_a?(Enumerable) && value.first.respond_to?(:save)
            # entity type
            ivar = send("#{name}")
            instance_variable_set("@#{name}", ivar.replace(value))
          elsif value.is_a?(Diametric::Associations::Collection)
            instance_variable_set("@#{name}", value)
          elsif value.is_a?(Enumerable)
            # enum type
            # however, falls here when empty array is given for entity type
            instance_variable_set("@#{name}", Set.new(value))
          end
        when :one
          if value.respond_to?(:save)
            # entity type
            if value.save
              instance_variable_set("@#{name}", value.dbid)
            end
          else
            # enum type
            instance_variable_set("@#{name}", value)
          end
        end
      else   # not Ref
        case cardinality
        when :many
          instance_variable_set("@#{name}", Set.new(value))
        when :one
          instance_variable_set("@#{name}", value)
        end
      end
    end

    define_method("clean_#{name}=") do |value|
      if self.class.attributes[name][:value_type] != Ref && cardinality == :many
        if value.is_a? Enumerable
          value = Set.new(value)
        else
          # used from rest reify
          ivar = instance_variable_get("@#{name}")
          ivar ||= Set.new
          value = ivar.add(value)
        end
      end
      if self.class.attributes[name][:value_type] == Ref &&
          cardinality == :many &&
          !(value.is_a? Diametric::Associations::Collection)
        if value.is_a? Enumerable
          value = Diametric::Associations::Collection.new(self, name, value)
        else
          value = Diametric::Associations::Collection.new(self, name, [value])
        end
      end
      instance_variable_set("@#{name}", value)
    end
  end
end

#partitionString

The Datomic partition this entity's data will be stored in. Defaults to :db.part/user.

Returns:

  • (String)

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/diametric/entity.rb', line 86

module ClassMethods
  def partition
    @partition
  end

  def partition=(partition)
    self.partition = partition.to_sym
  end

  def attributes
    @attributes
  end

  def defaults
    @defaults
  end

  # Set the namespace prefix used for attribute names
  #
  # @param prefix [#to_s] The prefix to be used for namespacing entity attributes
  #
  # @example Override the default namespace prefix
  #   class Mouse
  #     include Diametric::Entity
  #
  #     namespace_prefix :mice
  #   end
  #
  #   Mouse.new.prefix # => :mice
  #
  # @return void
  def namespace_prefix(prefix)
    @namespace_prefix = prefix
  end

  # Add an attribute to a {Diametric::Entity}.
  #
  # Valid options are:
  #
  # * +:index+: The only valid value is +true+. This causes the
  #   attribute to be indexed for easier lookup.
  # * +:unique+: Valid values are +:value+ or +:identity.+
  #   * +:value+ causes the attribute value to be unique to the
  #     entity and attempts to insert a duplicate value will fail.
  #   * +:identity+ causes the attribute value to be unique to
  #     the entity. Attempts to insert a duplicate value with a
  #     temporary entity id will result in an "upsert," causing the
  #     temporary entity's attributes to be merged with those for
  #     the current entity in Datomic.
  # * +:cardinality+: Specifies whether an attribute associates
  #   a single value or a set of values with an entity. The
  #   values allowed are:
  #   * +:one+ - the attribute is single valued, it associates a
  #     single value with an entity.
  #   * +:many+ - the attribute is mutli valued, it associates a
  #     set of values with an entity.
  #   +:one+ is the default.
  # * +:doc+: A string used in Datomic to document the attribute.
  # * +:fulltext+: The only valid value is +true+. Indicates that a
  #   fulltext search index should be generated for the attribute.
  # * +:default+: The value the attribute will default to when the
  #   Entity is initialized. Defaults for attributes with +:cardinality+ of +:many+
  #   will be transformed into a Set by passing the default to +Set.new+.
  #
  # @example Add an indexed name attribute.
  #   attribute :name, String, :index => true
  #
  # @param name [String] The attribute's name.
  # @param value_type [Class] The attribute's type.
  #   Must exist in {Diametric::Entity::VALUE_TYPES}.
  # @param opts [Hash] Options to pass to Datomic.
  #
  # @return void
  def attribute(name, value_type, opts = {})
    opts = DEFAULT_OPTIONS.merge(opts)

    establish_defaults(name, value_type, opts)

    @attributes[name] = {:value_type => value_type}.merge(opts)

    setup_attribute_methods(name, opts[:cardinality])
  end

  # @return [Array<Symbol>] Names of the entity's attributes.
  def attribute_names
    @attributes.keys
  end

  # @return [Array<Symbol>] Names of the entity's enums.
  def enum_names
    @enums.keys
  end

  # Add an enum to a {Diametric::Entity}.
  #
  # enum is used when attribute type is Ref and refers
  # a set of values.
  # name should be the same as corresponding attribute name.
  #
  # @example Add an enum of colors
  #   class Palette
  #     attribute :color, Ref
  #     enum :color, [:blue, :green, :yellow, :orange]
  #   end
  #   p = Pallet.new
  #   p.color = Pallet::Color::Green
  #
  # @param name [Symbol] The enum's name.
  # @param values [Array] The enum values.
  #
  # @return void
  def enum(name, values)
    enum_values = nil
    enum_values = values.to_set if values.is_a?(Array)
    enum_values = values if values.is_a?(Set)
    raise RuntimeError "values should be Array or Set" if enum_values.nil?
    enum_name = name.to_s.capitalize
    syms = values.collect(&:to_s).collect(&:upcase).collect(&:to_sym)
    class_eval("module #{enum_name};enum #{syms};end")
    @enums[name] = syms
  end

  # Generates a Datomic schema for a model's attributes.
  #
  # @return [Array] A Datomic schema, as Ruby data that can be
  #   converted to EDN.
  def schema
    defaults = {
      :"db/cardinality" => :"db.cardinality/one",
      :"db.install/_attribute" => :"db.part/db"
    }

    schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
      opts = opts.dup
      value_type = opts.delete(:value_type)

      unless opts.empty?
        opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
        opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
        opts = opts.map { |k, v|
          k = namespace("db", k)
          [k, v]
        }
        opts = Hash[*opts.flatten]
      end

      schema << defaults.merge({
                                 :"db/ident" => namespace(prefix, attribute),
                                 :"db/valueType" => value_type(value_type),
                                 :"db/id" => tempid(:"db.part/db")
                               }).merge(opts)
    end
    enum_schema = [
      :"db/add", tempid(:"db.part/user"), :"db/ident"
    ]
    prefix = self.to_s.underscore
    @enums.each do |key, values|
      values.each do |value|
        ident_value = :"#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
        es = [:"db/add", tempid(:"db.part/user"), :"db/ident", ident_value]
        schema_array << es
      end
    end
    schema_array
  end

  # Given a set of Ruby data returned from a Datomic query, this
  # can re-hydrate that data into a model instance.
  #
  # @return [Entity]
  def from_query(query_results, connection=nil, resolve=false)
    dbid = query_results.shift
    widget = self.new(Hash[attribute_names.zip query_results])
    widget.dbid = dbid

    if resolve
      widget = resolve_ref_dbid(widget, connection)
    end
    widget
  end

  def resolve_ref_dbid(parent, connection)
    parent.class.attribute_names.each do |e|
      if parent.class.attributes[e][:value_type] == "ref"
        ref = parent.instance_variable_get("@#{e.to_s}")
        if ref.is_a?(Fixnum) ||
          (self.instance_variable_get("@peer") && ref.is_a?(Diametric::Persistence::Entity))
          child = reify(ref, connection)
          child = resolve_ref_dbid(child, connection)
          parent.instance_variable_set("@#{e.to_s}", child)
        elsif ref.is_a?(Diametric::Associations::Collection)
          children = Diametric::Associations::Collection.new(parent, e.to_s)
          ref.each do |entity|
            children.add_reified_entities(reify(entity, connection))
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        elsif ref.is_a?(Set)
          children = ref.inject(Set.new) do |memo, entity|
            child = reify(entity, connection)
            memo.add(child)
            memo
          end
          parent.instance_variable_set("@#{e.to_s}", children)
        end
      end
    end
    parent
  end

  def reify(thing, conn_or_db=nil, resolve=false)
    return peer_reify(thing, conn_or_db, resolve) if self.instance_variable_get("@peer")
    rest_reify(thing, resolve)
  end

  def rest_reify(dbid, resolve)
    query = [
      :find, ~"?ident", ~"?v",
      :in, ~"\$", [~"?e"],
      :where, [~"?e", ~"?a", ~"?v"], [~"?a", :"db/ident", ~"?ident"]
    ]
    entities = self.q(query, [[dbid]])
    class_name = to_classname(entities.first.first)
    instance = eval("#{class_name}.new")
    entities.each do |k, v|
      matched_data = /([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(k.to_s)
      attribute = instance.send(matched_data[2])
      if attribute && attribute.is_a?(Diametric::Associations::Collection)
        attribute.add_reified_entities(v)
      else
        instance.send("clean_#{matched_data[2]}=", v)
      end
    end
    instance.send("dbid=", dbid)

    if resolve
      instance = resolve_ref_dbid(instance, nil)
    end

    instance
  end

  def peer_reify(thing, conn_or_db=nil, resolve=false)
    conn_or_db ||= Diametric::Persistence::Peer.connect.db

    if conn_or_db.respond_to?(:db)
      conn_or_db = conn_or_db.db
    end

    if thing.is_a? Fixnum
      dbid = thing
      entity = conn_or_db.entity(dbid)
    elsif thing.respond_to?(:eid)
      dbid = thing.eid
      if entity.respond_to?(:keys)
        entity = thing
      else
        entity = conn_or_db.entity(dbid)
      end
    elsif thing.kind_of? java.lang.Long
      entity = conn_or_db.entity(Diametric::Persistence::Object.new(thing))
    elsif thing.respond_to?(:to_java)
      dbid = thing.to_java
      entity = conn_or_db.entity(dbid)
    else
      return thing
    end
    first_key = entity.keys.first
    class_name = to_classname(first_key)
    instance = eval("#{class_name}.new")
    entity.keys.each do |key|
      matched_data = /:([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(key)
      instance.send("clean_#{matched_data[2]}=", entity[key])
    end
    instance.send("dbid=", Diametric::Persistence::Object.new(entity.get("db/id")))

    if resolve
      instance = resolve_ref_dbid(instance, conn_or_db)
    end

    instance
  end

  def to_classname(key)
    names = []
    # drops the first character ":"
    key = key.to_s
    if key[0] == ":"
      key = key[1..-1]
    end
    key.chars.inject("") do |memo, c|
      if c == "/"
        # means the end of class name
        names << memo
        break
      elsif c == "."
        # namespace delimiter
        names << memo
        memo = ""
      elsif c == "-"
        # Clojure uses - for name, but Ruby can't
        # converts dash to underscore
        memo << "_"
      else
        memo << c
      end
      memo
    end
    names.collect(&:camelize).join("::")
  end

  def find(id)
    if self.instance_variable_get("@peer")
      connection ||= Diametric::Persistence::Peer.connect
    end
    reify(id, connection)
  end

  # Create a temporary id placeholder.
  #
  # @param e [*#to_edn] Elements to put in the placeholder. Should
  #   be either partition or partition and a negative number to be
  #   used as a reference.
  #
  # @return [EDN::Type::Unknown] Temporary id placeholder.
  def tempid(*e)
    if self.instance_variable_get("@peer")
      Diametric::Persistence::Peer.tempid(*e)
    else
      Diametric::Persistence::REST.tempid(*e)
    end
  end

  # Returns the prefix for this model used in Datomic. Can be
  # overriden by declaring {#namespace_prefix}
  #
  # @example
  #   Mouse.prefix #=> "mouse"
  #   FireHouse.prefix #=> "fire_house"
  #   Person::User.prefix #=> "person.user"
  #
  # @return [String]
  def prefix
    @namespace_prefix || self.to_s.underscore.gsub('/', '.')
  end

  # Namespace a attribute for Datomic.
  #
  # @param ns [#to_s] Namespace.
  # @param attribute [#to_s] Attribute.
  #
  # @return [Symbol] Namespaced attribute.
  def namespace(ns, attribute)
    [ns.to_s, attribute.to_s].join("/").to_sym
  end

  # Raise an error if validation failed.
  #
  # @example Raise the validation error.
  #   Person.fail_validate!(person)
  #
  # @param [ Entity ] entity The entity to fail.
  def fail_validate!(entity)
    raise Errors::ValidationError.new(entity)
  end

  private

  def value_type(vt)
    if vt.is_a?(Class) || vt.is_a?(Module)
      vt = VALUE_TYPES[vt]
    end
    namespace("db.type", vt)
  end

  def establish_defaults(name, value_type, opts = {})
    default = opts.delete(:default)
    @defaults[name] = default if default
  end

  def setup_attribute_methods(name, cardinality)
    define_attribute_method name

    define_method(name) do
      ivar = instance_variable_get("@#{name}")
      if ivar.nil? &&
          self.class.attributes[name][:value_type] == Ref &&
          self.class.attributes[name][:cardinality] == :many
        ivar = Diametric::Associations::Collection.new(self, name)
      end
      ivar
    end

    define_method("#{name}=") do |value|
      send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
      case self.class.attributes[name][:value_type]
      when Ref
        case cardinality
        when :many
          if value.is_a?(Enumerable) && value.first.respond_to?(:save)
            # entity type
            ivar = send("#{name}")
            instance_variable_set("@#{name}", ivar.replace(value))
          elsif value.is_a?(Diametric::Associations::Collection)
            instance_variable_set("@#{name}", value)
          elsif value.is_a?(Enumerable)
            # enum type
            # however, falls here when empty array is given for entity type
            instance_variable_set("@#{name}", Set.new(value))
          end
        when :one
          if value.respond_to?(:save)
            # entity type
            if value.save
              instance_variable_set("@#{name}", value.dbid)
            end
          else
            # enum type
            instance_variable_set("@#{name}", value)
          end
        end
      else   # not Ref
        case cardinality
        when :many
          instance_variable_set("@#{name}", Set.new(value))
        when :one
          instance_variable_set("@#{name}", value)
        end
      end
    end

    define_method("clean_#{name}=") do |value|
      if self.class.attributes[name][:value_type] != Ref && cardinality == :many
        if value.is_a? Enumerable
          value = Set.new(value)
        else
          # used from rest reify
          ivar = instance_variable_get("@#{name}")
          ivar ||= Set.new
          value = ivar.add(value)
        end
      end
      if self.class.attributes[name][:value_type] == Ref &&
          cardinality == :many &&
          !(value.is_a? Diametric::Associations::Collection)
        if value.is_a? Enumerable
          value = Diametric::Associations::Collection.new(self, name, value)
        else
          value = Diametric::Associations::Collection.new(self, name, [value])
        end
      end
      instance_variable_set("@#{name}", value)
    end
  end
end

Instance Method Details

#attribute(name, value_type, opts = {}) ⇒ Object

Add an attribute to a Diametric::Entity.

Valid options are:

  • :index: The only valid value is true. This causes the attribute to be indexed for easier lookup.

  • :unique: Valid values are :value or :identity.

    • :value causes the attribute value to be unique to the entity and attempts to insert a duplicate value will fail.

    • :identity causes the attribute value to be unique to the entity. Attempts to insert a duplicate value with a temporary entity id will result in an “upsert,” causing the temporary entity's attributes to be merged with those for the current entity in Datomic.

  • :cardinality: Specifies whether an attribute associates a single value or a set of values with an entity. The values allowed are:

    • :one - the attribute is single valued, it associates a single value with an entity.

    • :many - the attribute is mutli valued, it associates a set of values with an entity.

    :one is the default.

  • :doc: A string used in Datomic to document the attribute.

  • :fulltext: The only valid value is true. Indicates that a fulltext search index should be generated for the attribute.

  • :default: The value the attribute will default to when the Entity is initialized. Defaults for attributes with :cardinality of :many will be transformed into a Set by passing the default to Set.new.

Examples:

Add an indexed name attribute.

attribute :name, String, :index => true

Parameters:

  • name (String)

    The attribute's name.

  • value_type (Class)

    The attribute's type. Must exist in VALUE_TYPES.

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

    Options to pass to Datomic.

Returns:

  • void


159
160
161
162
163
164
165
166
167
# File 'lib/diametric/entity.rb', line 159

def attribute(name, value_type, opts = {})
  opts = DEFAULT_OPTIONS.merge(opts)

  establish_defaults(name, value_type, opts)

  @attributes[name] = {:value_type => value_type}.merge(opts)

  setup_attribute_methods(name, opts[:cardinality])
end

#attribute_namesArray<Symbol>

Returns Names of the entity's attributes.

Returns:

  • (Array<Symbol>)

    Names of the entity's attributes.


170
171
172
# File 'lib/diametric/entity.rb', line 170

def attribute_names
  @attributes.keys
end

#enum(name, values) ⇒ Object

Add an enum to a Diametric::Entity.

enum is used when attribute type is Ref and refers a set of values. name should be the same as corresponding attribute name.

Examples:

Add an enum of colors

class Palette
  attribute :color, Ref
  enum :color, [:blue, :green, :yellow, :orange]
end
p = Pallet.new
p.color = Pallet::Color::Green

Parameters:

  • name (Symbol)

    The enum's name.

  • values (Array)

    The enum values.

Returns:

  • void


197
198
199
200
201
202
203
204
205
206
# File 'lib/diametric/entity.rb', line 197

def enum(name, values)
  enum_values = nil
  enum_values = values.to_set if values.is_a?(Array)
  enum_values = values if values.is_a?(Set)
  raise RuntimeError "values should be Array or Set" if enum_values.nil?
  enum_name = name.to_s.capitalize
  syms = values.collect(&:to_s).collect(&:upcase).collect(&:to_sym)
  class_eval("module #{enum_name};enum #{syms};end")
  @enums[name] = syms
end

#enum_namesArray<Symbol>

Returns Names of the entity's enums.

Returns:

  • (Array<Symbol>)

    Names of the entity's enums.


175
176
177
# File 'lib/diametric/entity.rb', line 175

def enum_names
  @enums.keys
end

#fail_validate!(entity) ⇒ Object

Raise an error if validation failed.

Examples:

Raise the validation error.

Person.fail_validate!(person)

Parameters:

  • entity (Entity)

    The entity to fail.

Raises:


447
448
449
# File 'lib/diametric/entity.rb', line 447

def fail_validate!(entity)
  raise Errors::ValidationError.new(entity)
end

#find(id) ⇒ Object


396
397
398
399
400
401
# File 'lib/diametric/entity.rb', line 396

def find(id)
  if self.instance_variable_get("@peer")
    connection ||= Diametric::Persistence::Peer.connect
  end
  reify(id, connection)
end

#from_query(query_results, connection = nil, resolve = false) ⇒ Entity

Given a set of Ruby data returned from a Datomic query, this can re-hydrate that data into a model instance.

Returns:


256
257
258
259
260
261
262
263
264
265
# File 'lib/diametric/entity.rb', line 256

def from_query(query_results, connection=nil, resolve=false)
  dbid = query_results.shift
  widget = self.new(Hash[attribute_names.zip query_results])
  widget.dbid = dbid

  if resolve
    widget = resolve_ref_dbid(widget, connection)
  end
  widget
end

#namespace(ns, attribute) ⇒ Symbol

Namespace a attribute for Datomic.

Parameters:

  • ns (#to_s)

    Namespace.

  • attribute (#to_s)

    Attribute.

Returns:

  • (Symbol)

    Namespaced attribute.


437
438
439
# File 'lib/diametric/entity.rb', line 437

def namespace(ns, attribute)
  [ns.to_s, attribute.to_s].join("/").to_sym
end

#namespace_prefix(prefix) ⇒ Object

Set the namespace prefix used for attribute names

Examples:

Override the default namespace prefix

class Mouse
  include Diametric::Entity

  namespace_prefix :mice
end

Mouse.new.prefix # => :mice

Parameters:

  • prefix (#to_s)

    The prefix to be used for namespacing entity attributes

Returns:

  • void


117
118
119
# File 'lib/diametric/entity.rb', line 117

def namespace_prefix(prefix)
  @namespace_prefix = prefix
end

#peer_reify(thing, conn_or_db = nil, resolve = false) ⇒ Object


327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/diametric/entity.rb', line 327

def peer_reify(thing, conn_or_db=nil, resolve=false)
  conn_or_db ||= Diametric::Persistence::Peer.connect.db

  if conn_or_db.respond_to?(:db)
    conn_or_db = conn_or_db.db
  end

  if thing.is_a? Fixnum
    dbid = thing
    entity = conn_or_db.entity(dbid)
  elsif thing.respond_to?(:eid)
    dbid = thing.eid
    if entity.respond_to?(:keys)
      entity = thing
    else
      entity = conn_or_db.entity(dbid)
    end
  elsif thing.kind_of? java.lang.Long
    entity = conn_or_db.entity(Diametric::Persistence::Object.new(thing))
  elsif thing.respond_to?(:to_java)
    dbid = thing.to_java
    entity = conn_or_db.entity(dbid)
  else
    return thing
  end
  first_key = entity.keys.first
  class_name = to_classname(first_key)
  instance = eval("#{class_name}.new")
  entity.keys.each do |key|
    matched_data = /:([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(key)
    instance.send("clean_#{matched_data[2]}=", entity[key])
  end
  instance.send("dbid=", Diametric::Persistence::Object.new(entity.get("db/id")))

  if resolve
    instance = resolve_ref_dbid(instance, conn_or_db)
  end

  instance
end

#prefixString

Returns the prefix for this model used in Datomic. Can be overriden by declaring #namespace_prefix

Examples:

Mouse.prefix #=> "mouse"
FireHouse.prefix #=> "fire_house"
Person::User.prefix #=> "person.user"

Returns:

  • (String)

427
428
429
# File 'lib/diametric/entity.rb', line 427

def prefix
  @namespace_prefix || self.to_s.underscore.gsub('/', '.')
end

#reify(thing, conn_or_db = nil, resolve = false) ⇒ Object


295
296
297
298
# File 'lib/diametric/entity.rb', line 295

def reify(thing, conn_or_db=nil, resolve=false)
  return peer_reify(thing, conn_or_db, resolve) if self.instance_variable_get("@peer")
  rest_reify(thing, resolve)
end

#resolve_ref_dbid(parent, connection) ⇒ Object


267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/diametric/entity.rb', line 267

def resolve_ref_dbid(parent, connection)
  parent.class.attribute_names.each do |e|
    if parent.class.attributes[e][:value_type] == "ref"
      ref = parent.instance_variable_get("@#{e.to_s}")
      if ref.is_a?(Fixnum) ||
        (self.instance_variable_get("@peer") && ref.is_a?(Diametric::Persistence::Entity))
        child = reify(ref, connection)
        child = resolve_ref_dbid(child, connection)
        parent.instance_variable_set("@#{e.to_s}", child)
      elsif ref.is_a?(Diametric::Associations::Collection)
        children = Diametric::Associations::Collection.new(parent, e.to_s)
        ref.each do |entity|
          children.add_reified_entities(reify(entity, connection))
        end
        parent.instance_variable_set("@#{e.to_s}", children)
      elsif ref.is_a?(Set)
        children = ref.inject(Set.new) do |memo, entity|
          child = reify(entity, connection)
          memo.add(child)
          memo
        end
        parent.instance_variable_set("@#{e.to_s}", children)
      end
    end
  end
  parent
end

#rest_reify(dbid, resolve) ⇒ Object


300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/diametric/entity.rb', line 300

def rest_reify(dbid, resolve)
  query = [
    :find, ~"?ident", ~"?v",
    :in, ~"\$", [~"?e"],
    :where, [~"?e", ~"?a", ~"?v"], [~"?a", :"db/ident", ~"?ident"]
  ]
  entities = self.q(query, [[dbid]])
  class_name = to_classname(entities.first.first)
  instance = eval("#{class_name}.new")
  entities.each do |k, v|
    matched_data = /([a-zA-Z0-9_\.]+)\/([a-zA-Z0-9_]+)/.match(k.to_s)
    attribute = instance.send(matched_data[2])
    if attribute && attribute.is_a?(Diametric::Associations::Collection)
      attribute.add_reified_entities(v)
    else
      instance.send("clean_#{matched_data[2]}=", v)
    end
  end
  instance.send("dbid=", dbid)

  if resolve
    instance = resolve_ref_dbid(instance, nil)
  end

  instance
end

#schemaArray

Generates a Datomic schema for a model's attributes.

Returns:

  • (Array)

    A Datomic schema, as Ruby data that can be converted to EDN.


212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/diametric/entity.rb', line 212

def schema
  defaults = {
    :"db/cardinality" => :"db.cardinality/one",
    :"db.install/_attribute" => :"db.part/db"
  }

  schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
    opts = opts.dup
    value_type = opts.delete(:value_type)

    unless opts.empty?
      opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
      opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
      opts = opts.map { |k, v|
        k = namespace("db", k)
        [k, v]
      }
      opts = Hash[*opts.flatten]
    end

    schema << defaults.merge({
                               :"db/ident" => namespace(prefix, attribute),
                               :"db/valueType" => value_type(value_type),
                               :"db/id" => tempid(:"db.part/db")
                             }).merge(opts)
  end
  enum_schema = [
    :"db/add", tempid(:"db.part/user"), :"db/ident"
  ]
  prefix = self.to_s.underscore
  @enums.each do |key, values|
    values.each do |value|
      ident_value = :"#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
      es = [:"db/add", tempid(:"db.part/user"), :"db/ident", ident_value]
      schema_array << es
    end
  end
  schema_array
end

#tempid(*e) ⇒ EDN::Type::Unknown

Create a temporary id placeholder.

Parameters:

  • e (*#to_edn)

    Elements to put in the placeholder. Should be either partition or partition and a negative number to be used as a reference.

Returns:

  • (EDN::Type::Unknown)

    Temporary id placeholder.


410
411
412
413
414
415
416
# File 'lib/diametric/entity.rb', line 410

def tempid(*e)
  if self.instance_variable_get("@peer")
    Diametric::Persistence::Peer.tempid(*e)
  else
    Diametric::Persistence::REST.tempid(*e)
  end
end

#to_classname(key) ⇒ Object


368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/diametric/entity.rb', line 368

def to_classname(key)
  names = []
  # drops the first character ":"
  key = key.to_s
  if key[0] == ":"
    key = key[1..-1]
  end
  key.chars.inject("") do |memo, c|
    if c == "/"
      # means the end of class name
      names << memo
      break
    elsif c == "."
      # namespace delimiter
      names << memo
      memo = ""
    elsif c == "-"
      # Clojure uses - for name, but Ruby can't
      # converts dash to underscore
      memo << "_"
    else
      memo << c
    end
    memo
  end
  names.collect(&:camelize).join("::")
end