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.
@@ -0,0 +1,88 @@
1
+ require 'diametric'
2
+ require 'diametric/persistence/common'
3
+ require 'datomic/client'
4
+
5
+ module Diametric
6
+ module Persistence
7
+ module REST
8
+ @connection = nil
9
+ @persisted_classes = Set.new
10
+
11
+ def self.included(base)
12
+ base.send(:include, Diametric::Persistence::Common)
13
+ base.send(:extend, ClassMethods)
14
+ @persisted_classes.add(base)
15
+ end
16
+
17
+ def self.connection
18
+ @connection
19
+ end
20
+
21
+ def self.create_schemas
22
+ @persisted_classes.each do |klass|
23
+ klass.create_schema
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def connect(uri, dbalias, database)
29
+ @uri = uri
30
+ @dbalias = dbalias
31
+ @database = database
32
+
33
+ @connection = Datomic::Client.new(uri, dbalias)
34
+ @connection.create_database(database)
35
+ end
36
+
37
+ def connection
38
+ @connection || Diametric::Persistence::REST.connection
39
+ end
40
+
41
+ def database
42
+ @database || Diametric::Persistence::REST.database
43
+ end
44
+
45
+ def transact(data)
46
+ connection.transact(database, data)
47
+ end
48
+
49
+ def get(dbid)
50
+ res = connection.entity(database, dbid)
51
+
52
+ # TODO tighten regex to only allow fields with the model name
53
+ attrs = res.data.map { |attr_symbol, value|
54
+ attr = attr_symbol.to_s.gsub(%r"^\w+/", '')
55
+ [attr, value]
56
+ }
57
+
58
+ entity = self.new(Hash[*attrs.flatten])
59
+ entity.dbid = dbid
60
+ entity
61
+ end
62
+
63
+ def q(query, args)
64
+ args.unshift(connection.db_alias(database))
65
+ res = connection.query(query, args)
66
+ res.data
67
+ end
68
+ end
69
+
70
+ extend ClassMethods
71
+
72
+ def save
73
+ return false unless valid?
74
+ return true unless changed?
75
+
76
+ res = self.class.transact(tx_data)
77
+ if dbid.nil?
78
+ self.dbid = res.data[:tempids].values.first
79
+ end
80
+
81
+ @previously_changed = changes
82
+ @changed_attributes.clear
83
+
84
+ res
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,157 @@
1
+ require 'diametric'
2
+
3
+ module Diametric
4
+ # +Query+ objects are used to generate Datomic queries, whether to
5
+ # send via an external client or via the persistence API. The two
6
+ # methods used to generate a query are +.where+ and +.filter+, both
7
+ # of which are chainable. To get the query data and arguments for a
8
+ # +Query+, use the +data+ method.
9
+ #
10
+ # If you are using a persistence API, you can ask +Query+ to get the
11
+ # results of a Datomic query. +Diametric::Query+ is an
12
+ # +Enumerable+. To get the results of a query, use +Enumerable+
13
+ # methods such as +.each+ or +.first+. +Query+ also provides a
14
+ # +.all+ method to run the query and get the results.
15
+ class Query
16
+ include Enumerable
17
+
18
+ attr_reader :conditions, :filters, :model
19
+
20
+ # Create a new Datomic query.
21
+ #
22
+ # @param model [Entity] This model must include +Datomic::Entity+. Including
23
+ # a persistence module is optional.
24
+ def initialize(model)
25
+ @model = model
26
+ @conditions = {}
27
+ @filters = []
28
+ end
29
+
30
+ # Add conditions to your Datomic query. Conditions check for equality
31
+ # against entity attributes. In addition, you can add conditions for
32
+ # use as variables in filters.
33
+ #
34
+ # @example Looking for mice named Wilbur.
35
+ # Query.new(Mouse).conditions(:name => "Wilbur")
36
+ #
37
+ # @param conditions [Hash] Datomic variables and values.
38
+ # @return [Query]
39
+ def where(conditions)
40
+ query = self.dup
41
+ query.conditions = query.conditions.merge(conditions)
42
+ query
43
+ end
44
+
45
+ # Add a filter to your Datomic query. Filters are known as expression clause
46
+ # predicates in the {Datomic query documentation}[http://docs.datomic.com/query.html].
47
+ #
48
+ # A filter can be in one of two forms. In the first, you pass a
49
+ # series of arguments. Any Ruby symbol given in this form will be
50
+ # converted to a EDN symbol. If the symbol is the same as one of
51
+ # the queried model's attributes or as a key passed to +where+, it
52
+ # will be prefixed with a +?+ so that it becomes a Datalog
53
+ # variable. In the second form, you pass
54
+ # {EDN}[https://github.com/relevance/edn-ruby] representing a
55
+ # Datomic predicate to +filter+. No conversion is done on this
56
+ # filter and it must be an EDN list.
57
+ #
58
+ # @example Passing arguments to be converted.
59
+ # query.filter(:>, :age, 21)
60
+ #
61
+ # @example Passing EDN, which will not be converted.
62
+ # query.filter(EDN::Type::List.new(EDN::Type::Symbol(">"),
63
+ # EDN::Type::Symbol("?age"),
64
+ # 21))
65
+ # # or, more simply
66
+ # query.filter(~[~">", ~"?age", 21])
67
+ #
68
+ # @param filter [Array] Either one +EDN::Type::List+ or a number of arguments
69
+ # that will be converted into a Datalog query.
70
+ # @return [Query]
71
+ def filter(*filter)
72
+ query = self.dup
73
+
74
+ if filter.first.is_a?(EDN::Type::List)
75
+ filter = filter.first
76
+ else
77
+ filter = filter.map { |e| convert_filter_element(e) }
78
+ filter = EDN::Type::List.new(*filter)
79
+ end
80
+
81
+ query.filters += [[filter]]
82
+ query
83
+ end
84
+
85
+ # Loop through the query results. In order to use +each+, your model *must*
86
+ # include a persistence API. At a minimum, it must have a +.q+ method that
87
+ # returns an +Enumerable+ object.
88
+ #
89
+ # @yield [Entity] An instance of the model passed to +Query+.
90
+ def each
91
+ # TODO check to see if the model has a `.q` method and give
92
+ # an appropriate error if not.
93
+ res = model.q(*data)
94
+ res.each do |entity|
95
+ # The map is for compatibility with Java peer persistence.
96
+ # TODO remove if possible
97
+ yield model.from_query(entity.map { |x| x })
98
+ end
99
+ end
100
+
101
+ # Return all query results.
102
+ #
103
+ # @return [Array<Entity>] Query results.
104
+ def all
105
+ map { |x| x }
106
+ end
107
+
108
+ # Create a Datomic query from the conditions and filters passed to this
109
+ # +Query+ object.
110
+ #
111
+ # @return [Array(Array, Array)] The first element of the array returned
112
+ # is the Datomic query composed of Ruby data. The second element is
113
+ # the arguments that used with the query.
114
+ def data
115
+ vars = model.attributes.map { |attribute, _, _| ~"?#{attribute}" }
116
+
117
+ from = conditions.map { |k, _| ~"?#{k}" }
118
+
119
+ clauses = model.attributes.map { |attribute, _, _|
120
+ [~"?e", model.namespace(model.prefix, attribute), ~"?#{attribute}"]
121
+ }
122
+ clauses += filters
123
+
124
+ args = conditions.map { |_, v| v }
125
+
126
+ query = [
127
+ :find, ~"?e", *vars,
128
+ :in, ~"\$", *from,
129
+ :where, *clauses
130
+ ]
131
+
132
+ [query, args]
133
+ end
134
+
135
+ protected
136
+
137
+ def conditions=(conditions)
138
+ @conditions = conditions
139
+ end
140
+
141
+ def filters=(filters)
142
+ @filters = filters
143
+ end
144
+
145
+ def convert_filter_element(element)
146
+ if element.is_a?(Symbol)
147
+ if model.attribute_names.include?(element) || @conditions.keys.include?(element)
148
+ EDN::Type::Symbol.new("?#{element}")
149
+ else
150
+ EDN::Type::Symbol.new(element.to_s)
151
+ end
152
+ else
153
+ element
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,3 @@
1
+ module Diametric
2
+ VERSION = "0.0.1"
3
+ end
data/lib/jrclj.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'java'
2
+
3
+ java_import "clojure.lang.RT"
4
+
5
+ class JRClj
6
+ def initialize *pkgs
7
+ @mappings = {}
8
+ @ns_map = RT.var "clojure.core", "ns-map"
9
+ @symbol = RT.var "clojure.core", "symbol"
10
+ @require = RT.var "clojure.core", "require"
11
+ _import "clojure.core"
12
+ pkgs.each do |pkg|
13
+ _import pkg
14
+ end
15
+ end
16
+
17
+ def _import pkg_name, sym=nil, sym_alias=nil
18
+ @require.invoke @symbol.invoke(pkg_name)
19
+ if sym
20
+ sym_alias ||= sym
21
+ @mappings[sym_alias] = RT.var pkg_name, sym
22
+ return
23
+ end
24
+ pkg = @symbol.invoke pkg_name
25
+ @ns_map.invoke(pkg).each do |sym,var|
26
+ @mappings[sym.to_s] = var
27
+ end
28
+ end
29
+
30
+ def _eval s
31
+ self.eval self.read_string(s)
32
+ end
33
+
34
+ def _invoke m, *args
35
+ fun = @mappings[m.to_s] || @mappings[m.to_s.gsub "_", "-"]
36
+ unless fun
37
+ raise "Error, no current binding for symbol=#{m}"
38
+ end
39
+ fun.invoke(*args)
40
+ end
41
+
42
+ def _alias new, old
43
+ @mappings[new] = @mappings[old]
44
+ end
45
+
46
+ def method_missing symbol, *args
47
+ _invoke symbol, *args
48
+ end
49
+
50
+ def self.persistent_map entries=[]
51
+ Java::ClojureLang::PersistentArrayMap.new entries.to_java
52
+ end
53
+
54
+ def edn_convert(obj)
55
+ self.read_string(obj.to_edn)
56
+ end
57
+ end
58
+
59
+ class Symbol
60
+ def to_clj
61
+ Java::ClojureLang::Keyword.intern(self.to_s)
62
+ end
63
+ end
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+
3
+ describe Diametric::Entity do
4
+ describe "in a class" do
5
+ subject { Person }
6
+
7
+ it { should respond_to(:attribute) }
8
+ it { should respond_to(:schema) }
9
+ it { should respond_to(:from_query) }
10
+
11
+ it "should generate a schema" do
12
+ Person.schema.should == [
13
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
14
+ :"db/ident" => :"person/name",
15
+ :"db/valueType" => :"db.type/string",
16
+ :"db/cardinality" => :"db.cardinality/one",
17
+ :"db/index" => true,
18
+ :"db.install/_attribute" => :"db.part/db" },
19
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
20
+ :"db/ident" => :"person/email",
21
+ :"db/valueType" => :"db.type/string",
22
+ :"db/cardinality" => :"db.cardinality/many",
23
+ :"db.install/_attribute" => :"db.part/db" },
24
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
25
+ :"db/ident" => :"person/birthday",
26
+ :"db/valueType" => :"db.type/instant",
27
+ :"db/cardinality" => :"db.cardinality/one",
28
+ :"db.install/_attribute" => :"db.part/db" },
29
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
30
+ :"db/ident" => :"person/awesome",
31
+ :"db/valueType" => :"db.type/boolean",
32
+ :"db/cardinality" => :"db.cardinality/one",
33
+ :"db/doc" => "Is this person awesome?",
34
+ :"db.install/_attribute" => :"db.part/db" },
35
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
36
+ :"db/ident" => :"person/ssn",
37
+ :"db/valueType" => :"db.type/string",
38
+ :"db/cardinality" => :"db.cardinality/one",
39
+ :"db/unique" => :"db.unique/value",
40
+ :"db.install/_attribute" => :"db.part/db" },
41
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
42
+ :"db/ident" => :"person/secret_name",
43
+ :"db/valueType" => :"db.type/string",
44
+ :"db/cardinality" => :"db.cardinality/one",
45
+ :"db/unique" => :"db.unique/identity",
46
+ :"db.install/_attribute" => :"db.part/db" },
47
+ { :"db/id" => Person.send(:tempid, :"db.part/db"),
48
+ :"db/ident" => :"person/bio",
49
+ :"db/valueType" => :"db.type/string",
50
+ :"db/cardinality" => :"db.cardinality/one",
51
+ :"db/fulltext" => true,
52
+ :"db.install/_attribute" => :"db.part/db" }
53
+ ]
54
+ end
55
+ end
56
+
57
+ describe "in an instance" do
58
+ subject { Person.new }
59
+ let(:model) { Person.new }
60
+
61
+ it_should_behave_like "ActiveModel"
62
+
63
+ it { should respond_to(:tx_data) }
64
+
65
+ it "should handle attributes correctly" do
66
+ subject.name.should be_nil
67
+ subject.name = "Clinton"
68
+ subject.name.should == "Clinton"
69
+ end
70
+ end
71
+
72
+ describe ".new" do
73
+ it "should work without arguments" do
74
+ Person.new.should be_a(Person)
75
+ end
76
+
77
+ it "should assign attributes based off argument keys" do
78
+ person = Person.new(:name => "Dashiell D", :secret_name => "Monito")
79
+ person.name.should == "Dashiell D"
80
+ person.secret_name.should == "Monito"
81
+ end
82
+ end
83
+
84
+ describe ".from_query" do
85
+ it "should assign dbid and attributes" do
86
+ goat = Goat.from_query([1, "Beans", DateTime.parse("1976/9/4")])
87
+ goat.dbid.should == 1
88
+ goat.name.should == "Beans"
89
+ goat.birthday.should == DateTime.parse("1976/9/4")
90
+ end
91
+ end
92
+
93
+ describe "#tx_data" do
94
+ let(:goat) { Goat.new(:name => "Beans", :birthday => Date.parse("2002-04-15"))}
95
+
96
+ describe "without a dbid" do
97
+ it "should generate a transaction with a new tempid" do
98
+ # Equivalence is currently wrong on EDN tagged values.
99
+ tx = goat.tx_data.first
100
+ tx.keys.should == [:"db/id", :"goat/name", :"goat/birthday"]
101
+ tx[:"db/id"].to_edn.should match(%r"#db/id \[:db.part/user \-\d+\]")
102
+ tx[:"goat/name"].should == "Beans"
103
+ tx[:"goat/birthday"].should == goat.birthday
104
+ end
105
+ end
106
+
107
+ describe "with a dbid" do
108
+ it "should generate a transaction with the dbid" do
109
+ goat.dbid = 1
110
+ goat.tx_data.should == [
111
+ { :"db/id" => 1,
112
+ :"goat/name" => "Beans",
113
+ :"goat/birthday" => goat.birthday
114
+ }
115
+ ]
116
+ end
117
+
118
+ it "should generate a transaction with only specified attributes" do
119
+ goat.dbid = 1
120
+ goat.tx_data(:name).should == [
121
+ { :"db/id" => 1,
122
+ :"goat/name" => "Beans"
123
+ }
124
+ ]
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'diametric/persistence/peer' if RUBY_ENGINE == 'jruby'
3
+
4
+ # Prevent CRuby from blowing up
5
+ module Diametric
6
+ module Persistence
7
+ module Peer
8
+ end
9
+ end
10
+ end
11
+
12
+ describe Diametric::Persistence::Peer, :jruby do
13
+ class Rat
14
+ include Diametric::Entity
15
+ include Diametric::Persistence::Peer
16
+
17
+ attribute :name, String, :index => true
18
+ attribute :age, Integer
19
+ end
20
+
21
+ let(:db_uri) { 'datomic:mem://hello' }
22
+
23
+ it "can connect to a Datomic database" do
24
+ subject.connect(db_uri)
25
+ subject.connection.should be_a(Java::DatomicPeer::LocalConnection)
26
+ end
27
+
28
+ it_behaves_like "persistence API" do
29
+ let(:model_class) { Rat }
30
+
31
+ before(:all) do
32
+ subject.connect(db_uri)
33
+ Diametric::Persistence::Peer.create_schemas
34
+ end
35
+ end
36
+ end