diametric 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,10 +5,21 @@ gemspec
5
5
 
6
6
  # Development-only dependencies
7
7
  gem 'rake'
8
- gem 'pry'
9
8
  gem 'rspec'
9
+ gem 'pry'
10
+
11
+ gem 'guard'
12
+ gem 'guard-rspec'
13
+ gem 'rb-inotify', :require => false
14
+ gem 'rb-fsevent', :require => false
15
+ gem 'rb-fchange', :require => false
10
16
 
11
17
  platform :mri do
12
- gem 'yard'
13
- gem 'redcarpet'
18
+ gem 'yard', :group => :development
19
+ gem 'redcarpet', :group => :development
20
+ end
21
+
22
+ platform :jruby do
23
+ gem 'jruby-openssl'
24
+ gem 'lock_jar'
14
25
  end
data/README.md CHANGED
@@ -1,9 +1,19 @@
1
+ [![Build Status](https://secure.travis-ci.org/relevance/diametric.png)](http://travis-ci.org/relevance/diametric)
2
+
1
3
  # Diametric
2
4
 
3
5
  Diametric is a library for building schemas, queries, and transactions
4
6
  for [Datomic][] from Ruby objects. It is also used to map Ruby objects
5
7
  as entities into a Datomic database.
6
8
 
9
+ ## Thanks
10
+
11
+ Development of Diametric was sponsored by [Relevance][]. They are the
12
+ best Clojure shop around and one of the best Ruby shops. I highly
13
+ recommend them for help with your corporate projects.
14
+
15
+ Special thanks to Mongoid for writing some solid ORM code that was liberally borrowed from to add Rails support to Diametric.
16
+
7
17
  ## Entity API
8
18
 
9
19
  The `Entity` module is interesting, in that it is primarily made of
@@ -150,7 +160,7 @@ require 'diametric/persistence/rest'
150
160
 
151
161
  # database url, database alias, database name
152
162
  # will create database if it does not already exist
153
- Diametric::Persistence::REST.connect('http://localhost:9000', 'test', 'animals')
163
+ Diametric::Persistence::REST.connect('http://localhost:9000', 'free', 'animals')
154
164
  ```
155
165
 
156
166
  ### Using persisted models
@@ -212,4 +222,19 @@ Or install it yourself as:
212
222
  4. Push to the branch (`git push origin my-new-feature`)
213
223
  5. Create new Pull Request
214
224
 
225
+ ## License
226
+
227
+ This project uses the [BSD License][].
228
+
229
+ Copyright (c) 2012, Clinton Dreisbach & Relevance Inc. All rights reserved.
230
+
231
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
232
+
233
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
234
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
235
+
236
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
237
+
215
238
  [Datomic]: http://www.datomic.com
239
+ [Relevance]: http://www.thinkrelevance.com
240
+ [BSD License]: http://opensource.org/licenses/BSD-2-Clause
data/Rakefile CHANGED
@@ -1,11 +1,13 @@
1
1
  begin
2
2
  require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
3
4
  rescue LoadError
4
5
  end
5
6
 
7
+
6
8
  task :default => :prepare
7
9
 
8
- task :prepare do
10
+ task :prepare => :install_lockjar do
9
11
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
10
12
  require 'lock_jar'
11
13
 
@@ -15,3 +17,17 @@ task :prepare do
15
17
  LockJar.install(lockfile)
16
18
  end
17
19
  end
20
+
21
+ task :install_lockjar do
22
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
23
+ require 'rubygems'
24
+ require 'rubygems/dependency_installer'
25
+ inst = Gem::DependencyInstaller.new
26
+ inst.install 'lock_jar', '~> 0.7.2'
27
+ end
28
+ end
29
+
30
+ desc "Run all RSpec tests"
31
+ require 'rspec'
32
+ require 'rspec/core/rake_task'
33
+ RSpec::Core::RakeTask.new(:spec)
@@ -6,26 +6,27 @@ require 'diametric/version'
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = "diametric"
8
8
  gem.version = Diametric::VERSION
9
- gem.authors = ["Clinton N. Dreisbach"]
10
- gem.email = ["crnixon@gmail.com"]
9
+ gem.authors = ["Clinton N. Dreisbach", "Ryan K. Neufeld", "Yoko Harada"]
10
+ gem.email = ["crnixon@gmail.com", "ryan@thinkrelevance.com", "yoko@thinkrelevance.com"]
11
11
  gem.summary = %q{ActiveModel for Datomic}
12
12
  gem.description = <<EOF
13
13
  Diametric is a library for building schemas, queries, and transactions
14
14
  for Datomic from Ruby objects. It is also used to map Ruby objects
15
15
  as entities into a Datomic database.
16
16
  EOF
17
- gem.homepage = "https://github.com/crnixon/diametric"
17
+ gem.homepage = "https://github.com/relevance/diametric"
18
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)/})
19
+ gem.files = %w(Gemfile Jarfile Jarfile.lock LICENSE.txt README.md Rakefile diametric.gemspec) + Dir.glob('lib/**/*')
20
+ gem.executables = []
21
+ gem.test_files = Dir.glob("spec/**/*.rb")
22
22
  gem.require_paths = ["lib"]
23
23
 
24
24
  gem.add_dependency 'edn', '~> 1.0'
25
25
  gem.add_dependency 'activesupport', '>= 3.0.0'
26
26
  gem.add_dependency 'activemodel', '>= 3.0.0'
27
27
  gem.add_dependency 'datomic-client', '~> 0.4.1'
28
- gem.add_dependency 'lock_jar', '~> 0.7.2'
28
+ gem.add_dependency 'rspec'
29
+ gem.add_dependency 'lock_jar', '= 0.7.2' if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
29
30
 
30
31
  gem.extensions = ['Rakefile']
31
32
  end
@@ -1,9 +1,17 @@
1
1
  require "diametric/version"
2
2
  require "diametric/entity"
3
3
  require "diametric/query"
4
+ require "diametric/persistence"
5
+
6
+ require 'diametric/config'
7
+
8
+ if defined?(Rails)
9
+ require 'diametric/railtie'
10
+ end
4
11
 
5
12
  # Diametric is a library for building schemas, queries, and
6
13
  # transactions for Datomic from Ruby objects. It is also used to map
7
14
  # Ruby objects as entities into a Datomic database.
8
15
  module Diametric
16
+ extend Diametric::Config
9
17
  end
@@ -0,0 +1,54 @@
1
+ require 'diametric/config/environment'
2
+ require 'diametric/persistence'
3
+
4
+ module Diametric
5
+ # Program-level configuration services including configuration loading and base connections.
6
+ module Config
7
+ extend self
8
+
9
+ # The current configuration Diametric will use in {#connect!}
10
+ #
11
+ # @return [Hash]
12
+ def configuration
13
+ @configuration ||= {}
14
+ end
15
+
16
+ # Determine if Diametric has been configured
17
+ #
18
+ # @return [Boolean]
19
+ def configured?
20
+ configuration.present?
21
+ end
22
+
23
+ # Load settings from a compliant diametric.yml file and make a connection. This can be used for
24
+ # easy setup with frameworks other than Rails.
25
+ #
26
+ # See {Persistence} for valid options.
27
+ #
28
+ # @example Configure Diametric.
29
+ # Diametric.load_and_connect!("/path/to/diametric.yml")
30
+ #
31
+ # @param [ String ] path The path to the file.
32
+ # @param [ String, Symbol ] environment The environment to load.
33
+ def load_and_connect!(path, environment = nil)
34
+ settings = Environment.load_yaml(path, environment)
35
+ @configuration = settings.with_indifferent_access
36
+ connect!(configuration)
37
+ peer_setup if Diametric::Persistence.peer?
38
+ configuration
39
+ end
40
+
41
+ # Establish a base connection from the supplied configuration hash.
42
+ #
43
+ # @param [ Hash ] configuration The configuration of the database to connect to. See {Persistence.establish_base_connection} for valid options.
44
+ def connect!(configuration)
45
+ ::Diametric::Persistence.establish_base_connection(configuration)
46
+ end
47
+
48
+ private
49
+ def peer_setup
50
+ load File.join(File.dirname(__FILE__), '..', 'tasks', 'create_schema.rb')
51
+ Rake::Task["diametric:create_schema_for_peer"].invoke
52
+ end
53
+ end
54
+ end
@@ -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
@@ -28,6 +28,7 @@ module Diametric
28
28
  # The database id assigned to the entity by Datomic.
29
29
  # @return [Integer]
30
30
  module Entity
31
+ Ref = "ref"
31
32
  # Conversions from Ruby types to Datomic types.
32
33
  VALUE_TYPES = {
33
34
  Symbol => "keyword",
@@ -39,6 +40,10 @@ module Diametric
39
40
  URI => "uri"
40
41
  }
41
42
 
43
+ DEFAULT_OPTIONS = {
44
+ :cardinality => :one
45
+ }
46
+
42
47
  @temp_ref = -1000
43
48
 
44
49
  def self.included(base)
@@ -46,9 +51,12 @@ module Diametric
46
51
  base.send(:extend, ActiveModel::Naming)
47
52
  base.send(:include, ActiveModel::Conversion)
48
53
  base.send(:include, ActiveModel::Dirty)
54
+ base.send(:include, ActiveModel::Validations)
49
55
 
50
56
  base.class_eval do
51
- @attributes = []
57
+ @attributes = {}
58
+ @defaults = {}
59
+ @namespace_prefix = nil
52
60
  @partition = :"db.part/user"
53
61
  end
54
62
  end
@@ -64,6 +72,9 @@ module Diametric
64
72
  #
65
73
  # @!attribute [r] attributes
66
74
  # @return [Array] Definitions of each of the entity's attributes (name, type, options).
75
+ #
76
+ # @!attribute [r] defaults
77
+ # @return [Array] Default values for any entitites defined with a +:default+ key and value.
67
78
  module ClassMethods
68
79
  def partition
69
80
  @partition
@@ -77,13 +88,35 @@ module Diametric
77
88
  @attributes
78
89
  end
79
90
 
91
+ def defaults
92
+ @defaults
93
+ end
94
+
95
+ # Set the namespace prefix used for attribute names
96
+ #
97
+ # @param prefix [#to_s] The prefix to be used for namespacing entity attributes
98
+ #
99
+ # @example Override the default namespace prefix
100
+ # class Mouse
101
+ # include Diametric::Entity
102
+ #
103
+ # namespace_prefix :mice
104
+ # end
105
+ #
106
+ # Mouse.new.prefix # => :mice
107
+ #
108
+ # @return void
109
+ def namespace_prefix(prefix)
110
+ @namespace_prefix = prefix
111
+ end
112
+
80
113
  # Add an attribute to a {Diametric::Entity}.
81
114
  #
82
115
  # Valid options are:
83
116
  #
84
117
  # * +:index+: The only valid value is +true+. This causes the
85
118
  # attribute to be indexed for easier lookup.
86
- # * +:unique+: Valid values are +:value+ or +:identity.
119
+ # * +:unique+: Valid values are +:value+ or +:identity.+
87
120
  # * +:value+ causes the attribute value to be unique to the
88
121
  # entity and attempts to insert a duplicate value will fail.
89
122
  # * +:identity+ causes the attribute value to be unique to
@@ -98,11 +131,13 @@ module Diametric
98
131
  # single value with an entity.
99
132
  # * +:many+ - the attribute is mutli valued, it associates a
100
133
  # 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
134
  # +:one+ is the default.
103
135
  # * +:doc+: A string used in Datomic to document the attribute.
104
136
  # * +:fulltext+: The only valid value is +true+. Indicates that a
105
137
  # fulltext search index should be generated for the attribute.
138
+ # * +:default+: The value the attribute will default to when the
139
+ # Entity is initialized. Defaults for attributes with +:cardinality+ of +:many+
140
+ # will be transformed into a Set by passing the default to +Set.new+.
106
141
  #
107
142
  # @example Add an indexed name attribute.
108
143
  # attribute :name, String, :index => true
@@ -114,20 +149,18 @@ module Diametric
114
149
  #
115
150
  # @return void
116
151
  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
152
+ opts = DEFAULT_OPTIONS.merge(opts)
153
+
154
+ establish_defaults(name, value_type, opts)
155
+
156
+ @attributes[name] = {:value_type => value_type}.merge(opts)
157
+
158
+ setup_attribute_methods(name, opts[:cardinality])
126
159
  end
127
160
 
128
161
  # @return [Array<Symbol>] Names of the entity's attributes.
129
162
  def attribute_names
130
- @attributes.map { |name, _, _| name }
163
+ @attributes.keys
131
164
  end
132
165
 
133
166
  # Generates a Datomic schema for a model's attributes.
@@ -141,10 +174,12 @@ module Diametric
141
174
  :"db.install/_attribute" => :"db.part/db"
142
175
  }
143
176
 
144
- @attributes.reduce([]) do |schema, (attribute, value_type, opts)|
177
+ @attributes.reduce([]) do |schema, (attribute, opts)|
145
178
  opts = opts.dup
179
+ value_type = opts.delete(:value_type)
180
+
146
181
  unless opts.empty?
147
- opts[:cardinality] = namespace("db.cardinality", opts[:cardinality]) if opts[:cardinality]
182
+ opts[:cardinality] = namespace("db.cardinality", opts[:cardinality])
148
183
  opts[:unique] = namespace("db.unique", opts[:unique]) if opts[:unique]
149
184
  opts = opts.map { |k, v|
150
185
  k = namespace("db", k)
@@ -166,12 +201,13 @@ module Diametric
166
201
  # @return [Entity]
167
202
  def from_query(query_results)
168
203
  dbid = query_results.shift
169
- widget = self.new(Hash[*(@attributes.map { |attribute, _, _| attribute }.zip(query_results).flatten)])
204
+ widget = self.new(Hash[attribute_names.zip query_results])
170
205
  widget.dbid = dbid
171
206
  widget
172
207
  end
173
208
 
174
- # Returns the prefix for this model used in Datomic.
209
+ # Returns the prefix for this model used in Datomic. Can be
210
+ # overriden by declaring {#namespace_prefix}
175
211
  #
176
212
  # @example
177
213
  # Mouse.prefix #=> "mouse"
@@ -180,7 +216,7 @@ module Diametric
180
216
  #
181
217
  # @return [String]
182
218
  def prefix
183
- self.to_s.underscore.sub('/', '.')
219
+ @namespace_prefix || self.to_s.underscore.sub('/', '.')
184
220
  end
185
221
 
186
222
  # Create a temporary id placeholder.
@@ -212,6 +248,27 @@ module Diametric
212
248
  end
213
249
  namespace("db.type", vt)
214
250
  end
251
+
252
+ def establish_defaults(name, value_type, opts = {})
253
+ default = opts.delete(:default)
254
+ @defaults[name] = default if default
255
+ end
256
+
257
+ def setup_attribute_methods(name, cardinality)
258
+ define_attribute_method name
259
+
260
+ define_method(name) do
261
+ instance_variable_get("@#{name}")
262
+ end
263
+
264
+ define_method("#{name}=") do |value|
265
+ send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")
266
+ if cardinality == :many
267
+ value = Set.new(value)
268
+ end
269
+ instance_variable_set("@#{name}", value)
270
+ end
271
+ end
215
272
  end
216
273
 
217
274
  def dbid
@@ -230,7 +287,7 @@ module Diametric
230
287
  # @param params [Hash] A hash of attributes and values to
231
288
  # initialize the entity with.
232
289
  def initialize(params = {})
233
- params.each do |k, v|
290
+ self.class.defaults.merge(params).each do |k, v|
234
291
  self.send("#{k}=", v)
235
292
  end
236
293
  end
@@ -243,19 +300,45 @@ module Diametric
243
300
 
244
301
  # Creates data for a Datomic transaction.
245
302
  #
246
- # @param attributes [*Symbol] Attributes to save in the
247
- # transaction. If no attributes are given, any changed
303
+ # @param attribute_names [*Symbol] Attribute names to save in the
304
+ # transaction. If no names are given, any changed
248
305
  # attributes will be saved.
249
306
  #
250
307
  # @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
308
+ def tx_data(*attribute_names)
309
+ attribute_names = self.changed_attributes.keys if attribute_names.empty?
310
+
311
+ entity_tx = {}
312
+ txes = []
313
+ attribute_names.each do |attribute_name|
314
+ cardinality = self.class.attributes[attribute_name.to_sym][:cardinality]
315
+
316
+ if cardinality == :many
317
+ txes += cardinality_many_tx_data(attribute_name)
318
+ else
319
+ entity_tx[self.class.namespace(self.class.prefix, attribute_name)] = self.send(attribute_name)
320
+ end
257
321
  end
258
- [tx]
322
+
323
+ if entity_tx.present?
324
+ txes << entity_tx.merge({:"db/id" => dbid || tempid})
325
+ end
326
+
327
+ txes
328
+ end
329
+
330
+ def cardinality_many_tx_data(attribute_name)
331
+ prev = Array(self.changed_attributes[attribute_name]).to_set
332
+ curr = self.send(attribute_name)
333
+
334
+ protractions = curr - prev
335
+ retractions = prev - curr
336
+
337
+ txes = []
338
+ namespaced_attribute = self.class.namespace(self.class.prefix, attribute_name)
339
+ txes << [:"db/retract", (dbid || tempid), namespaced_attribute, retractions.to_a] unless retractions.empty?
340
+ txes << [:"db/add", (dbid || tempid) , namespaced_attribute, protractions.to_a] unless protractions.empty?
341
+ txes
259
342
  end
260
343
 
261
344
  # @return [Array<Symbol>] Names of the entity's attributes.
@@ -335,5 +418,20 @@ module Diametric
335
418
  def errors
336
419
  @errors ||= ActiveModel::Errors.new(self)
337
420
  end
421
+
422
+ # Update method updates attribute values and saves new values in datomic.
423
+ # The method receives hash as an argument.
424
+ def update(attrs)
425
+ attrs.each do |k, v|
426
+ self.send(k.to_s+"=", v)
427
+ self.changed_attributes[k]=v
428
+ end
429
+ self.save if self.respond_to? :save
430
+ true
431
+ end
432
+
433
+ def destroy
434
+ self.retract_entity(dbid)
435
+ end
338
436
  end
339
437
  end