diametric 0.1.1-java

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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +28 -0
  3. data/Jarfile +20 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +264 -0
  6. data/Rakefile +49 -0
  7. data/bin/datomic-rest +33 -0
  8. data/bin/download-datomic +13 -0
  9. data/datomic_version.yml +4 -0
  10. data/diametric-java.gemspec +39 -0
  11. data/ext/diametric/DiametricCollection.java +147 -0
  12. data/ext/diametric/DiametricConnection.java +113 -0
  13. data/ext/diametric/DiametricDatabase.java +107 -0
  14. data/ext/diametric/DiametricEntity.java +90 -0
  15. data/ext/diametric/DiametricListenableFuture.java +47 -0
  16. data/ext/diametric/DiametricObject.java +66 -0
  17. data/ext/diametric/DiametricPeer.java +414 -0
  18. data/ext/diametric/DiametricService.java +102 -0
  19. data/ext/diametric/DiametricUUID.java +61 -0
  20. data/ext/diametric/DiametricUtils.java +183 -0
  21. data/lib/boolean_type.rb +3 -0
  22. data/lib/diametric.rb +42 -0
  23. data/lib/diametric/config.rb +54 -0
  24. data/lib/diametric/config/environment.rb +42 -0
  25. data/lib/diametric/entity.rb +659 -0
  26. data/lib/diametric/errors.rb +13 -0
  27. data/lib/diametric/generators/active_model.rb +42 -0
  28. data/lib/diametric/persistence.rb +48 -0
  29. data/lib/diametric/persistence/common.rb +82 -0
  30. data/lib/diametric/persistence/peer.rb +154 -0
  31. data/lib/diametric/persistence/rest.rb +107 -0
  32. data/lib/diametric/query.rb +259 -0
  33. data/lib/diametric/railtie.rb +52 -0
  34. data/lib/diametric/rest_service.rb +74 -0
  35. data/lib/diametric/service_base.rb +77 -0
  36. data/lib/diametric/transactor.rb +86 -0
  37. data/lib/diametric/version.rb +3 -0
  38. data/lib/diametric_service.jar +0 -0
  39. data/lib/tasks/create_schema.rb +27 -0
  40. data/lib/tasks/diametric_config.rb +45 -0
  41. data/lib/value_enums.rb +8 -0
  42. data/spec/conf_helper.rb +55 -0
  43. data/spec/config/free-transactor-template.properties +73 -0
  44. data/spec/config/logback.xml +59 -0
  45. data/spec/data/seattle-data0.dtm +452 -0
  46. data/spec/data/seattle-data1.dtm +326 -0
  47. data/spec/developer_create_sample.rb +39 -0
  48. data/spec/developer_query_spec.rb +120 -0
  49. data/spec/diametric/config_spec.rb +60 -0
  50. data/spec/diametric/entity_spec.rb +476 -0
  51. data/spec/diametric/peer_api_spec.rb +147 -0
  52. data/spec/diametric/persistence/peer_many2many_spec.rb +76 -0
  53. data/spec/diametric/persistence/peer_spec.rb +27 -0
  54. data/spec/diametric/persistence/rest_spec.rb +30 -0
  55. data/spec/diametric/persistence_spec.rb +59 -0
  56. data/spec/diametric/query_spec.rb +118 -0
  57. data/spec/diametric/rest_service_spec.rb +56 -0
  58. data/spec/diametric/transactor_spec.rb +68 -0
  59. data/spec/integration_spec.rb +107 -0
  60. data/spec/parent_child_sample.rb +42 -0
  61. data/spec/peer_integration_spec.rb +121 -0
  62. data/spec/peer_seattle_spec.rb +200 -0
  63. data/spec/rc2013_seattle_big.rb +82 -0
  64. data/spec/rc2013_seattle_small.rb +60 -0
  65. data/spec/rc2013_simple_sample.rb +72 -0
  66. data/spec/seattle_integration_spec.rb +106 -0
  67. data/spec/simple_validation_sample.rb +31 -0
  68. data/spec/spec_helper.rb +63 -0
  69. data/spec/support/entities.rb +157 -0
  70. data/spec/support/gen_entity_class.rb +9 -0
  71. data/spec/support/persistence_examples.rb +104 -0
  72. data/spec/test_version_file.yml +4 -0
  73. metadata +290 -0
@@ -0,0 +1,42 @@
1
+ module Diametric
2
+ module Config
3
+
4
+ # Encapsulates logic for getting environment information.
5
+ #
6
+ # This is nearly a word-for-word copy of {http://github.com/mongoid/mongoid Mongoid}'s {https://github.com/mongoid/mongoid/blob/master/lib/mongoid/config/environment.rb lib/mongoid/config/environment.rb} file.
7
+ # Thanks!
8
+ module Environment
9
+ extend self
10
+
11
+ # Get the name of the environment that we are running under. This first
12
+ # looks for Rails, then Sinatra, then a RACK_ENV environment variable,
13
+ # and if none of those are found raises an error.
14
+ #
15
+ # @example Get the env name.
16
+ # Environment.env_name
17
+ #
18
+ # @raise [ "No environment specified" ] If no environment was set.
19
+ #
20
+ # @return [ String ] The name of the current environment.
21
+ def env_name
22
+ return Rails.env if defined?(Rails)
23
+ return Sinatra::Base.environment.to_s if defined?(Sinatra)
24
+ ENV["RACK_ENV"] || ENV["DIAMETRIC_ENV"] || raise("No environment specified")
25
+ end
26
+
27
+ # Load the yaml from the provided path and return the settings for the
28
+ # current environment.
29
+ #
30
+ # @example Load the yaml.
31
+ # Environment.load_yaml("/work/diametric.yml")
32
+ #
33
+ # @param [ String ] path The location of the file.
34
+ #
35
+ # @return [ Hash ] The settings.
36
+ def load_yaml(path, environment = nil)
37
+ env = environment ? environment.to_s : env_name
38
+ YAML.load(ERB.new(File.new(path).read).result)[env]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,659 @@
1
+ require "bigdecimal"
2
+ require "edn"
3
+ require 'active_support/core_ext'
4
+ require 'active_support/inflector'
5
+ require 'active_model'
6
+ require 'set'
7
+ require 'value_enums'
8
+ require 'boolean_type'
9
+
10
+ module Diametric
11
+
12
+ # +Diametric::Entity+ is a module that, when included in a class,
13
+ # gives it the ability to generate Datomic schemas, queries, and
14
+ # transactions, and makes it +ActiveModel+ compliant.
15
+ #
16
+ # While this allows you to use this anywhere you would use an
17
+ # +ActiveRecord::Base+ model or another +ActiveModel+-compliant
18
+ # instance, it _does not_ include persistence. The +Entity+ module
19
+ # is primarily made of pure functions that take their receiver (an
20
+ # instance of the class they are included in) and return data that
21
+ # you can use in Datomic. +Entity+ can be best thought of as a data
22
+ # builder for Datomic.
23
+ #
24
+ # Of course, you can combine +Entity+ with one of the two available
25
+ # Diametric persistence modules for a fully persistent model.
26
+ #
27
+ # When +Entity+ is included in a class, that class is extended with
28
+ # {ClassMethods}.
29
+ #
30
+ # @!attribute dbid
31
+ # The database id assigned to the entity by Datomic.
32
+ # @return [Integer]
33
+ module Entity
34
+ Ref = "ref"
35
+ UUID = "uuid"
36
+ Double = "double"
37
+ # Conversions from Ruby types to Datomic types.
38
+ VALUE_TYPES = {
39
+ Symbol => "keyword",
40
+ String => "string",
41
+ Integer => "long",
42
+ Float => "double",
43
+ BigDecimal => "bigdec",
44
+ DateTime => "instant",
45
+ Boolean => "boolean",
46
+ URI => "uri",
47
+ UUID => "uuid"
48
+ }
49
+
50
+ DEFAULT_OPTIONS = {
51
+ :cardinality => :one
52
+ }
53
+
54
+ @temp_ref = -1000
55
+
56
+ def self.included(base)
57
+ base.send(:extend, ClassMethods)
58
+ base.send(:extend, ActiveModel::Naming)
59
+ base.send(:include, ActiveModel::Conversion)
60
+ base.send(:include, ActiveModel::Dirty)
61
+ base.send(:include, ActiveModel::Validations)
62
+
63
+ base.class_eval do
64
+ @attributes = {}
65
+ @defaults = {}
66
+ @enums = {}
67
+ @namespace_prefix = nil
68
+ @partition = :"db.part/user"
69
+ end
70
+ end
71
+
72
+ def self.next_temp_ref
73
+ @temp_ref -= 1
74
+ end
75
+
76
+ # @!attribute [rw] partition
77
+ # The Datomic partition this entity's data will be stored in.
78
+ # Defaults to +:db.part/user+.
79
+ # @return [String]
80
+ #
81
+ # @!attribute [r] attributes
82
+ # @return [Array] Definitions of each of the entity's attributes (name, type, options).
83
+ #
84
+ # @!attribute [r] defaults
85
+ # @return [Array] Default values for any entitites defined with a +:default+ key and value.
86
+ module ClassMethods
87
+ def partition
88
+ @partition
89
+ end
90
+
91
+ def partition=(partition)
92
+ self.partition = partition.to_sym
93
+ end
94
+
95
+ def attributes
96
+ @attributes
97
+ end
98
+
99
+ def defaults
100
+ @defaults
101
+ end
102
+
103
+ # Set the namespace prefix used for attribute names
104
+ #
105
+ # @param prefix [#to_s] The prefix to be used for namespacing entity attributes
106
+ #
107
+ # @example Override the default namespace prefix
108
+ # class Mouse
109
+ # include Diametric::Entity
110
+ #
111
+ # namespace_prefix :mice
112
+ # end
113
+ #
114
+ # Mouse.new.prefix # => :mice
115
+ #
116
+ # @return void
117
+ def namespace_prefix(prefix)
118
+ @namespace_prefix = prefix
119
+ end
120
+
121
+ # Add an attribute to a {Diametric::Entity}.
122
+ #
123
+ # Valid options are:
124
+ #
125
+ # * +:index+: The only valid value is +true+. This causes the
126
+ # attribute to be indexed for easier lookup.
127
+ # * +:unique+: Valid values are +:value+ or +:identity.+
128
+ # * +:value+ causes the attribute value to be unique to the
129
+ # entity and attempts to insert a duplicate value will fail.
130
+ # * +:identity+ causes the attribute value to be unique to
131
+ # the entity. Attempts to insert a duplicate value with a
132
+ # temporary entity id will result in an "upsert," causing the
133
+ # temporary entity's attributes to be merged with those for
134
+ # the current entity in Datomic.
135
+ # * +:cardinality+: Specifies whether an attribute associates
136
+ # a single value or a set of values with an entity. The
137
+ # values allowed are:
138
+ # * +:one+ - the attribute is single valued, it associates a
139
+ # single value with an entity.
140
+ # * +:many+ - the attribute is mutli valued, it associates a
141
+ # set of values with an entity.
142
+ # +:one+ is the default.
143
+ # * +:doc+: A string used in Datomic to document the attribute.
144
+ # * +:fulltext+: The only valid value is +true+. Indicates that a
145
+ # fulltext search index should be generated for the attribute.
146
+ # * +:default+: The value the attribute will default to when the
147
+ # Entity is initialized. Defaults for attributes with +:cardinality+ of +:many+
148
+ # will be transformed into a Set by passing the default to +Set.new+.
149
+ #
150
+ # @example Add an indexed name attribute.
151
+ # attribute :name, String, :index => true
152
+ #
153
+ # @param name [String] The attribute's name.
154
+ # @param value_type [Class] The attribute's type.
155
+ # Must exist in {Diametric::Entity::VALUE_TYPES}.
156
+ # @param opts [Hash] Options to pass to Datomic.
157
+ #
158
+ # @return void
159
+ def attribute(name, value_type, opts = {})
160
+ opts = DEFAULT_OPTIONS.merge(opts)
161
+
162
+ establish_defaults(name, value_type, opts)
163
+
164
+ @attributes[name] = {:value_type => value_type}.merge(opts)
165
+
166
+ setup_attribute_methods(name, opts[:cardinality])
167
+ end
168
+
169
+ # @return [Array<Symbol>] Names of the entity's attributes.
170
+ def attribute_names
171
+ @attributes.keys
172
+ end
173
+
174
+ # @return [Array<Symbol>] Names of the entity's enums.
175
+ def enum_names
176
+ @enums.keys
177
+ end
178
+
179
+ # Add an enum to a {Diametric::Entity}.
180
+ #
181
+ # enum is used when attribute type is Ref and refers
182
+ # a set of values.
183
+ # name should be the same as corresponding attribute name.
184
+ #
185
+ # @example Add an enum of colors
186
+ # class Palette
187
+ # attribute :color, Ref
188
+ # enum :color, [:blue, :green, :yellow, :orange]
189
+ # end
190
+ # p = Pallet.new
191
+ # p.color = Pallet::Color::Green
192
+ #
193
+ # @param name [Symbol] The enum's name.
194
+ # @param values [Array] The enum values.
195
+ #
196
+ # @return void
197
+ def enum(name, values)
198
+ enum_values = nil
199
+ enum_values = values.to_set if values.is_a?(Array)
200
+ enum_values = values if values.is_a?(Set)
201
+ raise RuntimeError "values should be Array or Set" if enum_values.nil?
202
+ enum_name = name.to_s.capitalize
203
+ syms = values.collect(&:to_s).collect(&:upcase).collect(&:to_sym)
204
+ class_eval("module #{enum_name};enum #{syms};end")
205
+ @enums[name] = syms
206
+ end
207
+
208
+ # Generates a Datomic schema for a model's attributes.
209
+ #
210
+ # @return [Array] A Datomic schema, as Ruby data that can be
211
+ # converted to EDN.
212
+ def schema
213
+ return peer_schema if self.instance_variable_get("@peer")
214
+ rest_schema
215
+ end
216
+
217
+ def rest_schema
218
+ defaults = {
219
+ :"db/id" => tempid(:"db.part/db"),
220
+ :"db/cardinality" => :"db.cardinality/one",
221
+ :"db.install/_attribute" => :"db.part/db"
222
+ }
223
+
224
+ schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
225
+ opts = opts.dup
226
+ value_type = opts.delete(:value_type)
227
+
228
+ unless opts.empty?
229
+ opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
230
+ opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
231
+ opts = opts.map { |k, v|
232
+ k = namespace("db", k)
233
+ [k, v]
234
+ }
235
+ opts = Hash[*opts.flatten]
236
+ end
237
+
238
+ schema << defaults.merge({
239
+ :"db/ident" => namespace(prefix, attribute),
240
+ :"db/valueType" => value_type(value_type),
241
+ }).merge(opts)
242
+ end
243
+
244
+ enum_schema = [
245
+ :"db/add", tempid(:"db.part/user"), :"db/ident"
246
+ ]
247
+ prefix = self.name.downcase
248
+ @enums.each do |key, values|
249
+ values.each do |value|
250
+ ident_value = :"#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
251
+ es = [:"db/add", tempid(:"db.part/user"), :"db/ident", ident_value]
252
+ schema_array << es
253
+ end
254
+ end
255
+ schema_array
256
+ end
257
+
258
+ # Generates a Datomic schema for a model's attributes.
259
+ #
260
+ # @return [Array] A Datomic schema, as Ruby data that can be
261
+ # converted to EDN.
262
+ def peer_schema
263
+ defaults = {
264
+ ":db/cardinality" => ":db.cardinality/one",
265
+ ":db.install/_attribute" => ":db.part/db"
266
+ }
267
+
268
+ schema_array = @attributes.reduce([]) do |schema, (attribute, opts)|
269
+ opts = opts.dup
270
+ value_type = opts.delete(:value_type)
271
+
272
+ unless opts.empty?
273
+ opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
274
+ opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
275
+ opts = opts.map { |k, v|
276
+ k = namespace("db", k)
277
+ [k, v]
278
+ }
279
+ opts = Hash[*opts.flatten]
280
+ end
281
+
282
+ schema << defaults.merge({
283
+ ":db/id" => Diametric::Persistence::Peer.tempid(":db.part/db"),
284
+ ":db/ident" => namespace(prefix, attribute),
285
+ ":db/valueType" => value_type(value_type),
286
+ }).merge(opts)
287
+ end
288
+
289
+ prefix = self.name.downcase
290
+ @enums.each do |key, values|
291
+ values.each do |value|
292
+ ident_value = ":#{prefix}.#{key.downcase}/#{value.to_s.sub(/_/, "-").downcase}"
293
+ es = [":db/add", Diametric::Persistence::Peer.tempid(":db.part/user"), ":db/ident", ident_value]
294
+ schema_array << es
295
+ end
296
+ end
297
+ schema_array
298
+ end
299
+
300
+ # Given a set of Ruby data returned from a Datomic query, this
301
+ # can re-hydrate that data into a model instance.
302
+ #
303
+ # @return [Entity]
304
+ def from_query(query_results, connection=nil, resolve=false)
305
+ dbid = query_results.shift
306
+ widget = self.new(Hash[attribute_names.zip query_results])
307
+ widget.dbid = dbid
308
+
309
+ if resolve
310
+ widget = resolve_ref_dbid(widget, connection)
311
+ end
312
+ widget
313
+ end
314
+
315
+ def resolve_ref_dbid(parent, connection)
316
+ parent.class.attribute_names.each do |e|
317
+ if parent.class.attributes[e][:value_type] == "ref"
318
+ ref = parent.instance_variable_get("@#{e.to_s}")
319
+ if ref.is_a?(Fixnum) || ref.is_a?(Java::DatomicQuery::EntityMap)
320
+ child = from_dbid_or_entity(ref, connection)
321
+ child = resolve_ref_dbid(child, connection)
322
+ parent.instance_variable_set("@#{e.to_s}", child)
323
+ elsif ref.is_a?(Set)
324
+ children = ref.inject(Set.new) do |memo, entity|
325
+ child = from_dbid_or_entity(entity, connection)
326
+ memo.add(child)
327
+ memo
328
+ end
329
+ parent.instance_variable_set("@#{e.to_s}", children)
330
+ end
331
+ end
332
+ end
333
+ parent
334
+ end
335
+
336
+ def from_dbid_or_entity(thing, conn_or_db=nil, resolve=false)
337
+ conn_or_db ||= Diametric::Persistence::Peer.connect.db
338
+
339
+ if conn_or_db.respond_to?(:db)
340
+ conn_or_db = conn_or_db.db
341
+ end
342
+
343
+ if thing.is_a? Fixnum
344
+ dbid = thing
345
+ entity = conn_or_db.entity(dbid)
346
+ elsif thing.respond_to?(:eid)
347
+ dbid = thing.eid
348
+ if entity.respond_to?(:keys)
349
+ entity = thing
350
+ else
351
+ entity = conn_or_db.entity(dbid)
352
+ end
353
+ elsif thing.respond_to?(:to_java)
354
+ dbid = thing.to_java
355
+ entity = conn_or_db.entity(dbid)
356
+ else
357
+ return thing
358
+ end
359
+ first_key = entity.keys.first
360
+ match_data = /:([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)/.match(first_key)
361
+ entity_name = match_data[1].capitalize.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}"}
362
+ instance = eval("#{entity_name}.new")
363
+ instance.send("#{match_data[2]}=", entity[first_key])
364
+ entity.keys[1..-1].each do |key|
365
+ match_data = /:([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)/.match(key)
366
+ instance.send("#{match_data[2]}=", entity[key])
367
+ end
368
+ instance.send("dbid=", Diametric::Persistence::Object.new(dbid))
369
+
370
+ if resolve
371
+ instance = resolve_ref_dbid(instance, conn_or_db)
372
+ end
373
+
374
+ instance
375
+ end
376
+
377
+ def find(id)
378
+ if self.instance_variable_get("@peer")
379
+ connection ||= Diametric::Persistence::Peer.connect
380
+ end
381
+ from_dbid_or_entity(id, connection)
382
+ end
383
+
384
+ # Returns the prefix for this model used in Datomic. Can be
385
+ # overriden by declaring {#namespace_prefix}
386
+ #
387
+ # @example
388
+ # Mouse.prefix #=> "mouse"
389
+ # FireHouse.prefix #=> "fire_house"
390
+ # Person::User.prefix #=> "person.user"
391
+ #
392
+ # @return [String]
393
+ def prefix
394
+ @namespace_prefix || self.to_s.underscore.sub('/', '.')
395
+ end
396
+
397
+ # Create a temporary id placeholder.
398
+ #
399
+ # @param e [*#to_edn] Elements to put in the placeholder. Should
400
+ # be either partition or partition and a negative number to be
401
+ # used as a reference.
402
+ #
403
+ # @return [EDN::Type::Unknown] Temporary id placeholder.
404
+ def tempid(*e)
405
+ if self.instance_variable_get("@peer")
406
+ if e[0].to_s.include?("user")
407
+ return Diametric::Persistence::Peer.tempid(":db.part/user")
408
+ else
409
+ return Diametric::Persistence::Peer.tempid(":db.part/db")
410
+ end
411
+ else
412
+ EDN.tagged_element('db/id', e)
413
+ end
414
+ end
415
+
416
+ # Namespace a attribute for Datomic.
417
+ #
418
+ # @param ns [#to_s] Namespace.
419
+ # @param attribute [#to_s] Attribute.
420
+ #
421
+ # @return [Symbol] Namespaced attribute.
422
+ def namespace(ns, attribute)
423
+ if self.instance_variable_get("@peer")
424
+ ":" + [ns.to_s, attribute.to_s].join("/")
425
+ else
426
+ [ns.to_s, attribute.to_s].join("/").to_sym
427
+ end
428
+ end
429
+
430
+ # Raise an error if validation failed.
431
+ #
432
+ # @example Raise the validation error.
433
+ # Person.fail_validate!(person)
434
+ #
435
+ # @param [ Entity ] entity The entity to fail.
436
+ def fail_validate!(entity)
437
+ raise Errors::ValidationError.new(entity)
438
+ end
439
+
440
+ private
441
+
442
+ def value_type(vt)
443
+ if vt.is_a?(Class) || vt.is_a?(Module)
444
+ vt = VALUE_TYPES[vt]
445
+ end
446
+ namespace("db.type", vt)
447
+ end
448
+
449
+ def establish_defaults(name, value_type, opts = {})
450
+ default = opts.delete(:default)
451
+ @defaults[name] = default if default
452
+ end
453
+
454
+ def setup_attribute_methods(name, cardinality)
455
+ define_attribute_method name
456
+
457
+ define_method(name) do
458
+ instance_variable_get("@#{name}")
459
+ end
460
+
461
+ define_method("#{name}=") do |value|
462
+ send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
463
+ if cardinality == :many
464
+ value = Set.new(value)
465
+ end
466
+ instance_variable_set("@#{name}", value)
467
+ end
468
+ end
469
+ end
470
+
471
+ def dbid
472
+ @dbid
473
+ end
474
+
475
+ def dbid=(dbid)
476
+ @dbid = dbid
477
+ end
478
+
479
+ alias :id :dbid
480
+ alias :"id=" :"dbid="
481
+
482
+ # Create a new {Diametric::Entity}.
483
+ #
484
+ # @param params [Hash] A hash of attributes and values to
485
+ # initialize the entity with.
486
+ def initialize(params = {})
487
+ self.class.defaults.merge(params).each do |k, v|
488
+ self.send("#{k}=", v)
489
+ end
490
+ end
491
+
492
+ # @return [Integer] A reference unique to this entity instance
493
+ # used in constructing temporary ids for transactions.
494
+ def temp_ref
495
+ @temp_ref ||= Diametric::Entity.next_temp_ref
496
+ end
497
+
498
+ # Creates data for a Datomic transaction.
499
+ #
500
+ # @param attribute_names [*Symbol] Attribute names to save in the
501
+ # transaction. If no names are given, any changed
502
+ # attributes will be saved.
503
+ #
504
+ # @return [Array] Datomic transaction data.
505
+ def tx_data(*attribute_names)
506
+ attribute_names = self.changed_attributes.keys if attribute_names.empty?
507
+
508
+ entity_tx = {}
509
+ txes = []
510
+ attribute_names.each do |attribute_name|
511
+ cardinality = self.class.attributes[attribute_name.to_sym][:cardinality]
512
+
513
+ #if cardinality == :many && self.class.instance_variable_get("@peer").nil?
514
+ if cardinality == :many
515
+ txes += cardinality_many_tx_data(attribute_name)
516
+ else
517
+ entity_tx[self.class.namespace(self.class.prefix, attribute_name)] = self.send(attribute_name)
518
+ end
519
+ end
520
+
521
+ if entity_tx.present?
522
+ if self.class.instance_variable_get("@peer")
523
+ @dbid ||= tempid
524
+ txes << entity_tx.merge({":db/id" => dbid})
525
+ else
526
+ txes << entity_tx.merge({:"db/id" => dbid || tempid})
527
+ end
528
+ end
529
+ txes
530
+ end
531
+
532
+ def cardinality_many_tx_data(attribute_name)
533
+ prev = Array(self.changed_attributes[attribute_name]).to_set
534
+ curr = self.send(attribute_name)
535
+
536
+ protractions = curr - prev
537
+ retractions = prev - curr
538
+
539
+ namespaced_attribute = self.class.namespace(self.class.prefix, attribute_name)
540
+ txes = []
541
+ if self.class.instance_variable_get("@peer")
542
+ @dbid ||= tempid
543
+ txes_data(txes, ":db/retract", namespaced_attribute, retractions) unless retractions.empty?
544
+ txes_data(txes, ":db/add", namespaced_attribute, protractions) unless protractions.empty?
545
+ else
546
+ txes << [:"db/retract", (dbid || tempid), namespaced_attribute, retractions.to_a] unless retractions.empty?
547
+ txes << [:"db/add", (dbid || tempid) , namespaced_attribute, protractions.to_a] unless protractions.empty?
548
+ end
549
+ txes
550
+ end
551
+
552
+ def txes_data(txes, op, namespaced_attribute, set)
553
+ set.to_a.each do |s|
554
+ value = s.respond_to?(:dbid) ? s.dbid : s
555
+ txes << [op, @dbid, namespaced_attribute, value]
556
+ end
557
+ end
558
+
559
+ # Returns hash of all attributes for this object
560
+ #
561
+ # @return [Hash<Symbol, object>] Hash of atrributes
562
+ def attributes
563
+ Hash[self.class.attribute_names.map {|attribute_name| [attribute_name, self.send(attribute_name)] }]
564
+ end
565
+
566
+ # @return [Array<Symbol>] Names of the entity's attributes.
567
+ def attribute_names
568
+ self.class.attribute_names
569
+ end
570
+
571
+ # @return [EDN::Type::Unknown] A temporary id placeholder for
572
+ # use in transactions.
573
+ def tempid
574
+ self.class.tempid(self.class.partition, temp_ref)
575
+ end
576
+
577
+ # Checks to see if the two objects are the same entity in Datomic.
578
+ #
579
+ # @return [Boolean]
580
+ def ==(other)
581
+ return false if self.dbid.nil?
582
+ return false unless other.respond_to?(:dbid)
583
+ return false unless self.dbid == other.dbid
584
+ true
585
+ end
586
+
587
+ # Checks to see if the two objects are of the same type, are the same
588
+ # entity, and have the same attribute values.
589
+ #
590
+ # @return [Boolean]
591
+ def eql?(other)
592
+ return false unless self == other
593
+ return false unless self.class == other.class
594
+
595
+ attribute_names.each do |attr|
596
+ return false unless self.send(attr) == other.send(attr)
597
+ end
598
+
599
+ true
600
+ end
601
+
602
+ # Methods for ActiveModel compliance.
603
+
604
+ # For ActiveModel compliance.
605
+ # @return [self]
606
+ def to_model
607
+ self
608
+ end
609
+
610
+ # Key for use in REST URL's, etc.
611
+ # @return [Integer, nil]
612
+ def to_key
613
+ persisted? ? [dbid] : nil
614
+ end
615
+
616
+ # @return [Boolean] Is the entity persisted?
617
+ def persisted?
618
+ !dbid.nil?
619
+ end
620
+
621
+ # @return [Boolean] Is this a new entity (that is, not persisted)?
622
+ def new_record?
623
+ !persisted?
624
+ end
625
+
626
+ # @return [false] Is this entity destroyed? (Always false.) For
627
+ # ActiveModel compliance.
628
+ def destroyed?
629
+ false
630
+ end
631
+
632
+ # @return [Boolean] Is this entity valid? By default, this is
633
+ # always true.
634
+ def valid?
635
+ errors.empty?
636
+ end
637
+
638
+ # @return [ActiveModel::Errors] Errors on this entity. By default,
639
+ # this is empty.
640
+ def errors
641
+ @errors ||= ActiveModel::Errors.new(self)
642
+ end
643
+
644
+ # Update method updates attribute values and saves new values in datomic.
645
+ # The method receives hash as an argument.
646
+ def update(attrs)
647
+ attrs.each do |k, v|
648
+ self.send(k.to_s+"=", v)
649
+ self.changed_attributes[k]=v
650
+ end
651
+ self.save if self.respond_to? :save
652
+ true
653
+ end
654
+
655
+ def destroy
656
+ self.retract_entity(dbid)
657
+ end
658
+ end
659
+ end