diametric 0.0.1

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