diametric 0.0.1

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