diametric 0.0.1

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.
data/diametric.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'diametric/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "diametric"
8
+ gem.version = Diametric::VERSION
9
+ gem.authors = ["Clinton N. Dreisbach"]
10
+ gem.email = ["crnixon@gmail.com"]
11
+ gem.summary = %q{ActiveModel for Datomic}
12
+ gem.description = <<EOF
13
+ Diametric is a library for building schemas, queries, and transactions
14
+ for Datomic from Ruby objects. It is also used to map Ruby objects
15
+ as entities into a Datomic database.
16
+ EOF
17
+ gem.homepage = "https://github.com/crnixon/diametric"
18
+
19
+ gem.files = `git ls-files`.split($/)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.require_paths = ["lib"]
23
+
24
+ gem.add_dependency 'edn', '~> 1.0'
25
+ gem.add_dependency 'activesupport', '>= 3.0.0'
26
+ gem.add_dependency 'activemodel', '>= 3.0.0'
27
+ gem.add_dependency 'datomic-client', '~> 0.4.1'
28
+ gem.add_dependency 'lock_jar', '~> 0.7.2'
29
+
30
+ gem.extensions = ['Rakefile']
31
+ end
data/lib/diametric.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "diametric/version"
2
+ require "diametric/entity"
3
+ require "diametric/query"
4
+
5
+ # Diametric is a library for building schemas, queries, and
6
+ # transactions for Datomic from Ruby objects. It is also used to map
7
+ # Ruby objects as entities into a Datomic database.
8
+ module Diametric
9
+ end
@@ -0,0 +1,339 @@
1
+ require "bigdecimal"
2
+ require "edn"
3
+ require 'active_support/core_ext'
4
+ require 'active_support/inflector'
5
+ require 'active_model'
6
+
7
+ module Diametric
8
+
9
+ # +Diametric::Entity+ is a module that, when included in a class,
10
+ # gives it the ability to generate Datomic schemas, queries, and
11
+ # transactions, and makes it +ActiveModel+ compliant.
12
+ #
13
+ # While this allows you to use this anywhere you would use an
14
+ # +ActiveRecord::Base+ model or another +ActiveModel+-compliant
15
+ # instance, it _does not_ include persistence. The +Entity+ module
16
+ # is primarily made of pure functions that take their receiver (an
17
+ # instance of the class they are included in) and return data that
18
+ # you can use in Datomic. +Entity+ can be best thought of as a data
19
+ # builder for Datomic.
20
+ #
21
+ # Of course, you can combine +Entity+ with one of the two available
22
+ # Diametric persistence modules for a fully persistent model.
23
+ #
24
+ # When +Entity+ is included in a class, that class is extended with
25
+ # {ClassMethods}.
26
+ #
27
+ # @!attribute dbid
28
+ # The database id assigned to the entity by Datomic.
29
+ # @return [Integer]
30
+ module Entity
31
+ # Conversions from Ruby types to Datomic types.
32
+ VALUE_TYPES = {
33
+ Symbol => "keyword",
34
+ String => "string",
35
+ Integer => "long",
36
+ Float => "float",
37
+ BigDecimal => "bigdec",
38
+ DateTime => "instant",
39
+ URI => "uri"
40
+ }
41
+
42
+ @temp_ref = -1000
43
+
44
+ def self.included(base)
45
+ base.send(:extend, ClassMethods)
46
+ base.send(:extend, ActiveModel::Naming)
47
+ base.send(:include, ActiveModel::Conversion)
48
+ base.send(:include, ActiveModel::Dirty)
49
+
50
+ base.class_eval do
51
+ @attributes = []
52
+ @partition = :"db.part/user"
53
+ end
54
+ end
55
+
56
+ def self.next_temp_ref
57
+ @temp_ref -= 1
58
+ end
59
+
60
+ # @!attribute [rw] partition
61
+ # The Datomic partition this entity's data will be stored in.
62
+ # Defaults to +:db.part/user+.
63
+ # @return [String]
64
+ #
65
+ # @!attribute [r] attributes
66
+ # @return [Array] Definitions of each of the entity's attributes (name, type, options).
67
+ module ClassMethods
68
+ def partition
69
+ @partition
70
+ end
71
+
72
+ def partition=(partition)
73
+ self.partition = partition.to_sym
74
+ end
75
+
76
+ def attributes
77
+ @attributes
78
+ end
79
+
80
+ # Add an attribute to a {Diametric::Entity}.
81
+ #
82
+ # Valid options are:
83
+ #
84
+ # * +:index+: The only valid value is +true+. This causes the
85
+ # attribute to be indexed for easier lookup.
86
+ # * +:unique+: Valid values are +:value+ or +:identity.
87
+ # * +:value+ causes the attribute value to be unique to the
88
+ # entity and attempts to insert a duplicate value will fail.
89
+ # * +:identity+ causes the attribute value to be unique to
90
+ # the entity. Attempts to insert a duplicate value with a
91
+ # temporary entity id will result in an "upsert," causing the
92
+ # temporary entity's attributes to be merged with those for
93
+ # the current entity in Datomic.
94
+ # * +:cardinality+: Specifies whether an attribute associates
95
+ # a single value or a set of values with an entity. The
96
+ # values allowed are:
97
+ # * +:one+ - the attribute is single valued, it associates a
98
+ # single value with an entity.
99
+ # * +:many+ - the attribute is mutli valued, it associates a
100
+ # set of values with an entity.
101
+ # To be honest, I have no idea how this will work in Ruby. Try +:many+ at your own risk.
102
+ # +:one+ is the default.
103
+ # * +:doc+: A string used in Datomic to document the attribute.
104
+ # * +:fulltext+: The only valid value is +true+. Indicates that a
105
+ # fulltext search index should be generated for the attribute.
106
+ #
107
+ # @example Add an indexed name attribute.
108
+ # attribute :name, String, :index => true
109
+ #
110
+ # @param name [String] The attribute's name.
111
+ # @param value_type [Class] The attribute's type.
112
+ # Must exist in {Diametric::Entity::VALUE_TYPES}.
113
+ # @param opts [Hash] Options to pass to Datomic.
114
+ #
115
+ # @return void
116
+ def attribute(name, value_type, opts = {})
117
+ @attributes << [name, value_type, opts]
118
+ define_attribute_method name
119
+ define_method(name) do
120
+ instance_variable_get("@#{name}")
121
+ end
122
+ define_method("#{name}=") do |value|
123
+ send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
124
+ instance_variable_set("@#{name}", value)
125
+ end
126
+ end
127
+
128
+ # @return [Array<Symbol>] Names of the entity's attributes.
129
+ def attribute_names
130
+ @attributes.map { |name, _, _| name }
131
+ end
132
+
133
+ # Generates a Datomic schema for a model's attributes.
134
+ #
135
+ # @return [Array] A Datomic schema, as Ruby data that can be
136
+ # converted to EDN.
137
+ def schema
138
+ defaults = {
139
+ :"db/id" => tempid(:"db.part/db"),
140
+ :"db/cardinality" => :"db.cardinality/one",
141
+ :"db.install/_attribute" => :"db.part/db"
142
+ }
143
+
144
+ @attributes.reduce([]) do |schema, (attribute, value_type, opts)|
145
+ opts = opts.dup
146
+ unless opts.empty?
147
+ opts[:cardinality] = namespace("db.cardinality", opts[:cardinality]) if opts[:cardinality]
148
+ opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
149
+ opts = opts.map { |k, v|
150
+ k = namespace("db", k)
151
+ [k, v]
152
+ }
153
+ opts = Hash[*opts.flatten]
154
+ end
155
+
156
+ schema << defaults.merge({
157
+ :"db/ident" => namespace(prefix, attribute),
158
+ :"db/valueType" => value_type(value_type),
159
+ }).merge(opts)
160
+ end
161
+ end
162
+
163
+ # Given a set of Ruby data returned from a Datomic query, this
164
+ # can re-hydrate that data into a model instance.
165
+ #
166
+ # @return [Entity]
167
+ def from_query(query_results)
168
+ dbid = query_results.shift
169
+ widget = self.new(Hash[*(@attributes.map { |attribute, _, _| attribute }.zip(query_results).flatten)])
170
+ widget.dbid = dbid
171
+ widget
172
+ end
173
+
174
+ # Returns the prefix for this model used in Datomic.
175
+ #
176
+ # @example
177
+ # Mouse.prefix #=> "mouse"
178
+ # FireHouse.prefix #=> "fire_house"
179
+ # Person::User.prefix #=> "person.user"
180
+ #
181
+ # @return [String]
182
+ def prefix
183
+ self.to_s.underscore.sub('/', '.')
184
+ end
185
+
186
+ # Create a temporary id placeholder.
187
+ #
188
+ # @param e [*#to_edn] Elements to put in the placeholder. Should
189
+ # be either partition or partition and a negative number to be
190
+ # used as a reference.
191
+ #
192
+ # @return [EDN::Type::Unknown] Temporary id placeholder.
193
+ def tempid(*e)
194
+ EDN.tagged_element('db/id', e)
195
+ end
196
+
197
+ # Namespace a attribute for Datomic.
198
+ #
199
+ # @param ns [#to_s] Namespace.
200
+ # @param attribute [#to_s] Attribute.
201
+ #
202
+ # @return [Symbol] Namespaced attribute.
203
+ def namespace(ns, attribute)
204
+ [ns.to_s, attribute.to_s].join("/").to_sym
205
+ end
206
+
207
+ private
208
+
209
+ def value_type(vt)
210
+ if vt.is_a?(Class)
211
+ vt = VALUE_TYPES[vt]
212
+ end
213
+ namespace("db.type", vt)
214
+ end
215
+ end
216
+
217
+ def dbid
218
+ @dbid
219
+ end
220
+
221
+ def dbid=(dbid)
222
+ @dbid = dbid
223
+ end
224
+
225
+ alias :id :dbid
226
+ alias :"id=" :"dbid="
227
+
228
+ # Create a new {Diametric::Entity}.
229
+ #
230
+ # @param params [Hash] A hash of attributes and values to
231
+ # initialize the entity with.
232
+ def initialize(params = {})
233
+ params.each do |k, v|
234
+ self.send("#{k}=", v)
235
+ end
236
+ end
237
+
238
+ # @return [Integer] A reference unique to this entity instance
239
+ # used in constructing temporary ids for transactions.
240
+ def temp_ref
241
+ @temp_ref ||= Diametric::Entity.next_temp_ref
242
+ end
243
+
244
+ # Creates data for a Datomic transaction.
245
+ #
246
+ # @param attributes [*Symbol] Attributes to save in the
247
+ # transaction. If no attributes are given, any changed
248
+ # attributes will be saved.
249
+ #
250
+ # @return [Array] Datomic transaction data.
251
+ def tx_data(*attributes)
252
+ tx = {:"db/id" => dbid || tempid}
253
+ attributes = self.changed_attributes.keys if attributes.empty?
254
+ attributes.reduce(tx) do |t, attribute|
255
+ t[self.class.namespace(self.class.prefix, attribute)] = self.send(attribute)
256
+ t
257
+ end
258
+ [tx]
259
+ end
260
+
261
+ # @return [Array<Symbol>] Names of the entity's attributes.
262
+ def attribute_names
263
+ self.class.attribute_names
264
+ end
265
+
266
+ # @return [EDN::Type::Unknown] A temporary id placeholder for
267
+ # use in transactions.
268
+ def tempid
269
+ self.class.tempid(self.class.partition, temp_ref)
270
+ end
271
+
272
+ # Checks to see if the two objects are the same entity in Datomic.
273
+ #
274
+ # @return [Boolean]
275
+ def ==(other)
276
+ return false if self.dbid.nil?
277
+ return false unless other.respond_to?(:dbid)
278
+ return false unless self.dbid == other.dbid
279
+ true
280
+ end
281
+
282
+ # Checks to see if the two objects are of the same type, are the same
283
+ # entity, and have the same attribute values.
284
+ #
285
+ # @return [Boolean]
286
+ def eql?(other)
287
+ return false unless self == other
288
+ return false unless self.class == other.class
289
+
290
+ attribute_names.each do |attr|
291
+ return false unless self.send(attr) == other.send(attr)
292
+ end
293
+
294
+ true
295
+ end
296
+
297
+ # Methods for ActiveModel compliance.
298
+
299
+ # For ActiveModel compliance.
300
+ # @return [self]
301
+ def to_model
302
+ self
303
+ end
304
+
305
+ # Key for use in REST URL's, etc.
306
+ # @return [Integer, nil]
307
+ def to_key
308
+ persisted? ? [dbid] : nil
309
+ end
310
+
311
+ # @return [Boolean] Is the entity persisted?
312
+ def persisted?
313
+ !dbid.nil?
314
+ end
315
+
316
+ # @return [Boolean] Is this a new entity (that is, not persisted)?
317
+ def new_record?
318
+ !persisted?
319
+ end
320
+
321
+ # @return [false] Is this entity destroyed? (Always false.) For
322
+ # ActiveModel compliance.
323
+ def destroyed?
324
+ false
325
+ end
326
+
327
+ # @return [Boolean] Is this entity valid? By default, this is
328
+ # always true.
329
+ def valid?
330
+ errors.empty?
331
+ end
332
+
333
+ # @return [ActiveModel::Errors] Errors on this entity. By default,
334
+ # this is empty.
335
+ def errors
336
+ @errors ||= ActiveModel::Errors.new(self)
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,29 @@
1
+ module Diametric
2
+ module Persistence
3
+ module Common
4
+ def self.included(base)
5
+ base.send(:extend, ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def create_schema
10
+ transact(schema)
11
+ end
12
+
13
+ def first(conditions = {})
14
+ where(conditions).first
15
+ end
16
+
17
+ def where(conditions = {})
18
+ query = Diametric::Query.new(self)
19
+ query.where(conditions)
20
+ end
21
+
22
+ def filter(*filter)
23
+ query = Diametric::Query.new(self)
24
+ query.filter(*filter)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,107 @@
1
+ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
2
+ raise "This module requires the use of JRuby."
3
+ end
4
+
5
+ require 'diametric'
6
+ require 'diametric/persistence/common'
7
+
8
+ require 'java'
9
+ require 'lock_jar'
10
+ lockfile = File.expand_path( "../../../../Jarfile.lock", __FILE__ )
11
+ # Loads the classpath with Jars from the lockfile
12
+ LockJar.load(lockfile)
13
+
14
+ require 'jrclj'
15
+ java_import "clojure.lang.Keyword"
16
+
17
+ module Diametric
18
+ module Persistence
19
+ module Peer
20
+ include_package "datomic"
21
+
22
+ @connection = nil
23
+ @persisted_classes = Set.new
24
+
25
+ def self.included(base)
26
+ base.send(:include, Diametric::Persistence::Common)
27
+ base.send(:extend, ClassMethods)
28
+ @persisted_classes.add(base)
29
+ end
30
+
31
+ def self.connection
32
+ @connection
33
+ end
34
+
35
+ def self.create_schemas
36
+ @persisted_classes.each do |klass|
37
+ klass.create_schema
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+ include_package "datomic"
43
+
44
+ def connect(uri)
45
+ Java::Datomic::Peer.create_database(uri)
46
+ @connection = Java::Datomic::Peer.connect(uri)
47
+ end
48
+
49
+ def disconnect
50
+ @connection = nil
51
+ end
52
+
53
+ def connection
54
+ @connection || Diametric::Persistence::Peer.connection
55
+ end
56
+
57
+ def transact(data)
58
+ data = clj.edn_convert(data)
59
+ res = connection.transact(data)
60
+ res.get
61
+ end
62
+
63
+ def get(dbid)
64
+ entity_map = connection.db.entity(dbid)
65
+ attrs = entity_map.key_set.map { |attr_keyword|
66
+ attr = attr_keyword.to_s.gsub(%r"^:\w+/", '')
67
+ value = entity_map.get(attr_keyword)
68
+ [attr, value]
69
+ }
70
+
71
+ entity = self.new(Hash[*attrs.flatten])
72
+ entity.dbid = dbid
73
+
74
+ entity
75
+ end
76
+
77
+ def q(query, args)
78
+ Java::Datomic::Peer.q(clj.edn_convert(query), connection.db, *args)
79
+ end
80
+
81
+ def clj
82
+ @clj ||= JRClj.new
83
+ end
84
+ end
85
+
86
+ extend ClassMethods
87
+
88
+ def save
89
+ return false unless valid?
90
+ return true unless changed?
91
+
92
+ res = self.class.transact(tx_data)
93
+ if dbid.nil?
94
+ self.dbid = Java::Datomic::Peer.resolve_tempid(
95
+ res[:"db-after".to_clj],
96
+ res[:tempids.to_clj],
97
+ self.class.clj.edn_convert(tempid))
98
+ end
99
+
100
+ @previously_changed = changes
101
+ @changed_attributes.clear
102
+
103
+ res
104
+ end
105
+ end
106
+ end
107
+ end