rie 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/lib/rie.rb +10 -0
- data/lib/rie/attribute.rb +149 -0
- data/lib/rie/base_changer.rb +93 -0
- data/lib/rie/base_finder.rb +99 -0
- data/lib/rie/has_database.rb +21 -0
- data/lib/rie/model.rb +225 -0
- data/lib/rie/schema.rb +112 -0
- data/lib/rie/validator.rb +105 -0
- data/lib/rie/version.rb +5 -0
- data/rie.gemspec +20 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4f20eb6f5e275de66f7eecb8776d6b050d778fd0
|
4
|
+
data.tar.gz: 26672d08cfeee732ea10de1d5ac688ded3b92ff0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8cb9a6c21a61de5238cf33b34a5719d343d2ee229799c02acc017349d184dec8ed5f2002c2f8e50b428f2d9686db34a9757e656d0ce00b88e75519c65e40d151
|
7
|
+
data.tar.gz: e89b60722ac59e0b22587d1dab000a2027eae8a0fc71deb62ddec855aa7700675d5b746b781d8ca3ecd1afaf2ea5fa0c8642d1ba7fa075421585edc5d800e3d2
|
data/Gemfile
ADDED
data/lib/rie.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'logger' # stdlib
|
2
|
+
require 'dalton'
|
3
|
+
|
4
|
+
load_dir = Pathname.new(__FILE__).dirname
|
5
|
+
load load_dir.join('rie/model.rb')
|
6
|
+
load load_dir.join('rie/schema.rb')
|
7
|
+
load load_dir.join('rie/attribute.rb')
|
8
|
+
load load_dir.join('rie/base_finder.rb')
|
9
|
+
load load_dir.join('rie/base_changer.rb')
|
10
|
+
load load_dir.join('rie/validator.rb')
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Rie
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :model, :datomic_attribute, :type
|
4
|
+
def initialize(model, name, opts={})
|
5
|
+
@name = name
|
6
|
+
@model = model
|
7
|
+
@datomic_attribute = opts.fetch(:datomic_attribute) { default_datomic_attribute }
|
8
|
+
@type = Type.for(opts[:type])
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_datomic_attribute
|
12
|
+
"#{model.namespace}.#{model.datomic_name}/#{name.to_s.tr('_', '-')}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(value)
|
16
|
+
type.load(self, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def dump(value)
|
20
|
+
type.dump(self, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def datoms_for(changer, value, &b)
|
24
|
+
type.datoms_for(self, changer, value, &b)
|
25
|
+
end
|
26
|
+
|
27
|
+
class Type
|
28
|
+
def self.for(definition)
|
29
|
+
return definition if definition.is_a? Type
|
30
|
+
return AutoType.new if definition.nil?
|
31
|
+
|
32
|
+
type, *args = definition
|
33
|
+
type_name = "#{type.capitalize}Type"
|
34
|
+
raise "no such type #{type}" unless const_defined?(type_name)
|
35
|
+
const_get(type_name).new(*args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def load(attr, value)
|
39
|
+
value
|
40
|
+
end
|
41
|
+
|
42
|
+
def dump(attr, value)
|
43
|
+
value
|
44
|
+
end
|
45
|
+
|
46
|
+
def datoms_for(attr, changer, value, &b)
|
47
|
+
yield(:'db/id' => changer.id, attr.datomic_attribute => dump(attr, value))
|
48
|
+
end
|
49
|
+
|
50
|
+
def invalid_value!(attr, value)
|
51
|
+
raise ::TypeError, "invalid value for #{attr.datomic_attribute}: #{value.inspect}"
|
52
|
+
end
|
53
|
+
|
54
|
+
class Scalar < Type
|
55
|
+
def inspect
|
56
|
+
"(scalar)"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class AutoType < Type
|
61
|
+
def type_from_value(value)
|
62
|
+
case value
|
63
|
+
when Enumerable
|
64
|
+
SetType.new(self)
|
65
|
+
when Dalton::Entity
|
66
|
+
RefType.new(raise 'TODO')
|
67
|
+
when Numeric, String, Symbol, true, false, nil
|
68
|
+
Scalar.new
|
69
|
+
else
|
70
|
+
raise TypeError.new("unknown value type: #{value.inspect}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def load(attr, value)
|
75
|
+
type_from_value(value).load(attr, value)
|
76
|
+
end
|
77
|
+
|
78
|
+
def dump(attr, value)
|
79
|
+
type_from_value(value).dump(attr, value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def datoms_for(attr, changer, value, &b)
|
83
|
+
type_from_value(value).datoms_for(attr, changer, value, &b)
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
"(auto)"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class RefType < Type
|
92
|
+
attr_reader :ref_class
|
93
|
+
def initialize(ref_class)
|
94
|
+
@ref_class = ref_class
|
95
|
+
end
|
96
|
+
|
97
|
+
def inspect
|
98
|
+
"(ref #{@ref_class.name})"
|
99
|
+
end
|
100
|
+
|
101
|
+
def load(attr, entity_map)
|
102
|
+
return nil if entity_map.nil?
|
103
|
+
registry_name = entity_map.get(attr.model.datomic_type_key)
|
104
|
+
invalid_value!(attr, entity_map) unless registry_name == @ref_class.datomic_type
|
105
|
+
@ref_class.new(entity_map)
|
106
|
+
end
|
107
|
+
|
108
|
+
def dump(attr, value)
|
109
|
+
value.id
|
110
|
+
end
|
111
|
+
|
112
|
+
def datoms_for(attr, changer, value, &block)
|
113
|
+
invalid_value!(attr, value) unless value.respond_to? :id
|
114
|
+
|
115
|
+
yield(:'db/id' => changer.id, attr.datomic_attribute => value.id)
|
116
|
+
|
117
|
+
value.generate_datoms(&block) if value.is_a? BaseChanger
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class SetType < Type
|
122
|
+
def initialize(element_type)
|
123
|
+
@element_type = Type.for(element_type)
|
124
|
+
end
|
125
|
+
|
126
|
+
def inspect
|
127
|
+
"(set #{@element_type.inspect})"
|
128
|
+
end
|
129
|
+
|
130
|
+
def dump(attr, value)
|
131
|
+
value.map { |x| @element_type.dump(value) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def load(attr, value)
|
135
|
+
# empty sets are often returned as nil in datomic :[
|
136
|
+
return Set.new if value.nil?
|
137
|
+
invalid_value!(attr, value) unless value.is_a? Enumerable
|
138
|
+
Set.new(value.map { |e| @element_type.load(attr, e) })
|
139
|
+
end
|
140
|
+
|
141
|
+
def datoms_for(attr, changer, value, &block)
|
142
|
+
value.each do |v|
|
143
|
+
@element_type.datoms_for(attr, changer, v, &block)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rie
|
2
|
+
class BaseChanger
|
3
|
+
attr_reader :id, :original, :changes, :retractions
|
4
|
+
def initialize(id, attrs)
|
5
|
+
@id = id
|
6
|
+
@original = attrs.dup.freeze
|
7
|
+
@changes = {}
|
8
|
+
@retractions = Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def retract!(attribute)
|
12
|
+
@retractions << attribute
|
13
|
+
end
|
14
|
+
|
15
|
+
def change(key=nil, &b)
|
16
|
+
b.call(self)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def change_ref(key, &b)
|
21
|
+
attribute = model.get_attribute(key)
|
22
|
+
type = attribute.type
|
23
|
+
|
24
|
+
unless type.respond_to? :ref_class
|
25
|
+
raise ::TypeError, "change_ref only works on refs - #{key} is a #{type.inspect} is not a ref"
|
26
|
+
end
|
27
|
+
|
28
|
+
self[key] = self[key] ? self[key].change(&b) : type.ref_class.create(&b)
|
29
|
+
end
|
30
|
+
|
31
|
+
def change!(&b)
|
32
|
+
change(&b)
|
33
|
+
save!
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](key)
|
37
|
+
return nil if @retractions.include? key
|
38
|
+
@changes.fetch(key) { @original[key] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def original(key)
|
42
|
+
@original[key]
|
43
|
+
end
|
44
|
+
|
45
|
+
def change_in(key)
|
46
|
+
[original(key), self[key]]
|
47
|
+
end
|
48
|
+
|
49
|
+
def []=(key, val)
|
50
|
+
if val.nil?
|
51
|
+
@retractions << key
|
52
|
+
else
|
53
|
+
@retractions.delete(key)
|
54
|
+
@changes[key] = val
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def updated_attributes
|
59
|
+
out = model.attributes.merge(@changes)
|
60
|
+
@retractions.each { |r| out.delete(r) }
|
61
|
+
out
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_datoms(&b)
|
65
|
+
return enum_for(:generate_datoms).to_a unless block_given?
|
66
|
+
|
67
|
+
yield model.base_attributes.merge(:'db/id' => @id)
|
68
|
+
@changes.each do |key, new_val|
|
69
|
+
attribute = model.get_attribute(key)
|
70
|
+
attribute.datoms_for(self, new_val, &b)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def save!
|
76
|
+
validate!
|
77
|
+
persist!
|
78
|
+
end
|
79
|
+
|
80
|
+
def persist!
|
81
|
+
result = model.transact(generate_datoms)
|
82
|
+
@id = result.resolve_tempid(@id) unless @id.is_a? Fixnum
|
83
|
+
model.new(result.db_after.entity(@id))
|
84
|
+
rescue Dalton::TypeError, Dalton::UniqueConflict => e
|
85
|
+
raise TransactionValidationError.new(self, e)
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate!
|
89
|
+
model.validator.run_all!(self)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Rie
|
2
|
+
class NotFound < StandardError
|
3
|
+
attr_reader :model, :id
|
4
|
+
def initialize(model, id)
|
5
|
+
@model = model
|
6
|
+
@id = id
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
"Could not find #{model} with id #{id}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class BaseFinder
|
15
|
+
include Enumerable
|
16
|
+
include Dalton::Utility
|
17
|
+
|
18
|
+
# should be overridden automatically
|
19
|
+
def model
|
20
|
+
raise "abstract"
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
translated = Dalton::Translation.from_ruby(all_constraints).to_edn[1..-2]
|
25
|
+
"#<#{self.class.name} ##{db.basis_t} :where #{translated}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :db, :constraints
|
29
|
+
def initialize(db, constraints=[])
|
30
|
+
@db = db
|
31
|
+
@constraints = constraints
|
32
|
+
end
|
33
|
+
|
34
|
+
def where(*constraints)
|
35
|
+
new_constraints = @constraints.dup
|
36
|
+
constraints.each do |c|
|
37
|
+
case c
|
38
|
+
when Array
|
39
|
+
new_constraints << c
|
40
|
+
when Hash
|
41
|
+
interpret_constraints(c, &new_constraints.method(:<<))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
self.class.new(@db, new_constraints)
|
46
|
+
end
|
47
|
+
|
48
|
+
def entity(id)
|
49
|
+
entity = @db.entity(id)
|
50
|
+
|
51
|
+
unless entity.get(model.datomic_type_key) == model.datomic_type
|
52
|
+
raise NotFound.new(model, id)
|
53
|
+
end
|
54
|
+
|
55
|
+
model.new(entity)
|
56
|
+
end
|
57
|
+
|
58
|
+
def results
|
59
|
+
query = [:find, sym('?e'), :in, sym('$'), :where, *all_constraints]
|
60
|
+
q(query).lazy.map do |el|
|
61
|
+
model.new(@db.entity(el.first))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def type_constraint
|
66
|
+
[sym('?e'), model.datomic_type_key, model.datomic_type]
|
67
|
+
end
|
68
|
+
|
69
|
+
def all_constraints
|
70
|
+
[type_constraint, *constraints]
|
71
|
+
end
|
72
|
+
|
73
|
+
def each(&b)
|
74
|
+
results.each(&b)
|
75
|
+
end
|
76
|
+
|
77
|
+
def with_model(model)
|
78
|
+
model.finder(@db)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def interpret_constraints(hash, &b)
|
84
|
+
return enum_for(:interpret_constraints, hash) unless block_given?
|
85
|
+
|
86
|
+
hash.each do |key, value|
|
87
|
+
attribute = model.get_attribute(key)
|
88
|
+
yield [sym('?e'), attribute.datomic_attribute, attribute.dump(value)]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def q(query)
|
93
|
+
translated_query = Dalton::Translation.from_ruby(query)
|
94
|
+
Model.logger.info("datomic.q #{translated_query.to_edn}")
|
95
|
+
result = @db.q(translated_query)
|
96
|
+
Dalton::Translation.from_clj(result)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rie
|
2
|
+
module HasDatabase
|
3
|
+
attr_accessor :db
|
4
|
+
|
5
|
+
def datomic_uri
|
6
|
+
raise "please define datomic_uri on #{self.class.name}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def datomic_connection
|
10
|
+
Dalton::Connection.connect(datomic_uri)
|
11
|
+
end
|
12
|
+
|
13
|
+
def refresh_datomic!
|
14
|
+
@db = datomic_connection.db
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(model, *args)
|
18
|
+
model.finder(@db, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/rie/model.rb
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
module Rie
|
2
|
+
module Model
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
@attributes = {}
|
6
|
+
@base_attributes = {}
|
7
|
+
@defaults = {}
|
8
|
+
@validator = Validator.new(base)
|
9
|
+
|
10
|
+
const_set :Finder, Class.new(BaseFinder) {
|
11
|
+
# we use a constant here so that `super` works
|
12
|
+
# in overriding generated methods
|
13
|
+
const_set :AttributeMethods, Module.new
|
14
|
+
include self::AttributeMethods
|
15
|
+
define_method(:model) { base }
|
16
|
+
}
|
17
|
+
|
18
|
+
const_set :Changer, Class.new(BaseChanger) {
|
19
|
+
# as above
|
20
|
+
const_set :AttributeMethods, Module.new
|
21
|
+
include self::AttributeMethods
|
22
|
+
define_method(:model) { base }
|
23
|
+
}
|
24
|
+
|
25
|
+
extend Rie::Model::ClassMethods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@registry = {}
|
30
|
+
@logger = Logger.new($stderr)
|
31
|
+
@logger.level = Logger::WARN
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_reader :registry
|
35
|
+
attr_writer :logger
|
36
|
+
|
37
|
+
def install_schemas!
|
38
|
+
registry.values.each(&:install_schema!)
|
39
|
+
end
|
40
|
+
|
41
|
+
def install_bases!
|
42
|
+
registry.values.each(&:install_base!)
|
43
|
+
end
|
44
|
+
|
45
|
+
def install!
|
46
|
+
install_bases!
|
47
|
+
install_schemas!
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_accessor :namespace, :partition, :uri, :logger
|
51
|
+
def configure(&b)
|
52
|
+
yield self
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
attr_reader :attributes
|
58
|
+
attr_reader :defaults
|
59
|
+
attr_reader :validator
|
60
|
+
attr_reader :datomic_name
|
61
|
+
attr_reader :namespace
|
62
|
+
attr_reader :partition
|
63
|
+
attr_reader :base_attributes
|
64
|
+
|
65
|
+
def transact(edn)
|
66
|
+
Model.logger.info("datomic.transact #{Dalton::Connection.convert_datoms(edn).to_edn}")
|
67
|
+
connection.transact(edn)
|
68
|
+
end
|
69
|
+
|
70
|
+
def base_attribute(key, val)
|
71
|
+
@base_attributes.merge!(key => val)
|
72
|
+
end
|
73
|
+
|
74
|
+
def uri(arg=nil)
|
75
|
+
@uri = arg if arg
|
76
|
+
@uri or Model.uri or raise "you must specify a datomic uri for #{self}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def connection
|
80
|
+
Dalton::Connection.connect(uri)
|
81
|
+
end
|
82
|
+
|
83
|
+
def datomic_type
|
84
|
+
:"#{namespace}.type/#{datomic_name}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def datomic_type_key
|
88
|
+
:"#{namespace}/type"
|
89
|
+
end
|
90
|
+
|
91
|
+
def attribute(attr, datomic_key=nil, opts={})
|
92
|
+
if datomic_key.is_a? Hash
|
93
|
+
opts = datomic_key
|
94
|
+
datomic_key = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
datomic_key ||= "#{self.namespace}.#{self.datomic_name}/#{attr.to_s.tr('_', '-')}"
|
98
|
+
define_attribute(attr, datomic_key, opts)
|
99
|
+
end
|
100
|
+
|
101
|
+
def define_attribute(key, datomic_key, opts={})
|
102
|
+
@attributes[key] = Attribute.new(self, key, opts.merge(datomic_attribute: datomic_key))
|
103
|
+
@defaults[key] = opts[:default]
|
104
|
+
|
105
|
+
define_method(key) { self[key] }
|
106
|
+
|
107
|
+
self::Finder::AttributeMethods.class_eval do
|
108
|
+
define_method("by_#{key}") { |v| where(key => v) }
|
109
|
+
end
|
110
|
+
|
111
|
+
self::Changer::AttributeMethods.class_eval do
|
112
|
+
define_method(key) { self[key] }
|
113
|
+
define_method("#{key}=") { |v| self[key] = v }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_attribute(key)
|
118
|
+
@attributes.fetch(key) do
|
119
|
+
raise ArgumentError, "Undefined attribute #{key} for #{self}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def finders(&b)
|
124
|
+
self::Finder.class_eval(&b)
|
125
|
+
end
|
126
|
+
|
127
|
+
def changers(&b)
|
128
|
+
self::Changer.class_eval(&b)
|
129
|
+
end
|
130
|
+
|
131
|
+
def validation(&b)
|
132
|
+
@validator.specify(&b)
|
133
|
+
end
|
134
|
+
|
135
|
+
def finder(db, constraints=[])
|
136
|
+
self::Finder.new(db).where(constraints)
|
137
|
+
end
|
138
|
+
|
139
|
+
def create!(&b)
|
140
|
+
self::Changer.new(Dalton::Utility.tempid(partition), defaults).change!(&b)
|
141
|
+
end
|
142
|
+
|
143
|
+
def create(&b)
|
144
|
+
self::Changer.new(Dalton::Utility.tempid(partition), defaults).change(&b)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
attr_reader :finder, :entity
|
149
|
+
def initialize(entity)
|
150
|
+
@entity = entity
|
151
|
+
@finder = self.class::Finder.new(entity.db)
|
152
|
+
end
|
153
|
+
|
154
|
+
def id
|
155
|
+
entity.get(:'db/id')
|
156
|
+
end
|
157
|
+
|
158
|
+
def db
|
159
|
+
entity.db
|
160
|
+
end
|
161
|
+
|
162
|
+
def at(db)
|
163
|
+
self.class::Finder.new(db).entity(self.id)
|
164
|
+
end
|
165
|
+
|
166
|
+
def [](key)
|
167
|
+
definition = self.class.get_attribute(key)
|
168
|
+
|
169
|
+
definition.load(entity.get(definition.datomic_attribute))
|
170
|
+
end
|
171
|
+
|
172
|
+
def interpret_value(value)
|
173
|
+
case value
|
174
|
+
when Enumerable
|
175
|
+
value.lazy.map { |e| interpret_value(e) }
|
176
|
+
when Java::DatomicQuery::EntityMap
|
177
|
+
self.class.interpret_entity(value)
|
178
|
+
when Numeric, String, Symbol, true, false, nil
|
179
|
+
value
|
180
|
+
else
|
181
|
+
raise TypeError.new("unknown value type: #{value.inspect}")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def attributes
|
186
|
+
out = {}
|
187
|
+
|
188
|
+
self.class.attributes.each do |attr, _|
|
189
|
+
out[attr] = send(attr)
|
190
|
+
end
|
191
|
+
|
192
|
+
out
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_h
|
196
|
+
attributes.merge(:id => id)
|
197
|
+
end
|
198
|
+
|
199
|
+
# TODO: fix this implementation
|
200
|
+
def updated_at
|
201
|
+
txid = db.q('[:find (max ?t) :in $ ?e :where [?e _ _ ?t]]', self.id).first.first
|
202
|
+
db.entity(txid).get(:'db/txInstant').to_time
|
203
|
+
end
|
204
|
+
|
205
|
+
def changer
|
206
|
+
self.class::Changer.new(id, attributes)
|
207
|
+
end
|
208
|
+
|
209
|
+
def change(&b)
|
210
|
+
changer.change(&b)
|
211
|
+
end
|
212
|
+
|
213
|
+
def change!(&b)
|
214
|
+
changer.change!(&b)
|
215
|
+
end
|
216
|
+
|
217
|
+
def retract!
|
218
|
+
self.class.transact([[:'db.fn/retractEntity', self.id]])
|
219
|
+
end
|
220
|
+
|
221
|
+
def ==(other)
|
222
|
+
self.entity == other.entity
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
data/lib/rie/schema.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Rie
|
2
|
+
module Model
|
3
|
+
module ClassMethods
|
4
|
+
def schema(name=nil, opts={}, &b)
|
5
|
+
return @schema unless block_given?
|
6
|
+
|
7
|
+
if name.is_a? Hash
|
8
|
+
opts = name
|
9
|
+
name = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
@datomic_name = name
|
13
|
+
@datomic_name ||= self.name
|
14
|
+
.gsub(/[^[:alpha:]]+/, '-')
|
15
|
+
.gsub(/(?<=[[:lower:]])(?=[[:upper:]])/, '-')
|
16
|
+
.downcase
|
17
|
+
|
18
|
+
@namespace = opts.fetch(:namespace) { Model.namespace } \
|
19
|
+
or raise ArgumentError.new("no namespace configured for #{self} or globally")
|
20
|
+
@partition = opts.fetch(:partition) { Model.partition } \
|
21
|
+
or raise ArgumentError.new("no partition configured for #{self} or globally")
|
22
|
+
@partition = :"db.part/#{partition}" unless partition.to_s.start_with?('db.part/')
|
23
|
+
|
24
|
+
Model.registry[datomic_type.to_s] = self
|
25
|
+
base_attribute datomic_type_key, datomic_type
|
26
|
+
|
27
|
+
@schema = Schema.new(self, &b)
|
28
|
+
end
|
29
|
+
|
30
|
+
def install_schema!
|
31
|
+
raise ArgumentError.new("no schema defined for #{self}!") unless schema
|
32
|
+
schema.install!
|
33
|
+
end
|
34
|
+
|
35
|
+
def install_base!
|
36
|
+
transact <<-EDN
|
37
|
+
[{:db/id #db/id[:db.part/db]
|
38
|
+
:db/ident :#{partition}
|
39
|
+
:db.install/_partition :db.part/db}
|
40
|
+
|
41
|
+
{:db/id #db/id[:db.part/db]
|
42
|
+
:db/ident :#{namespace}/type
|
43
|
+
:db/valueType :db.type/ref
|
44
|
+
:db/cardinality :db.cardinality/one
|
45
|
+
:db/doc "A model's type"
|
46
|
+
:db.install/_attribute :db.part/db}]
|
47
|
+
EDN
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Schema
|
53
|
+
include Dalton::Utility
|
54
|
+
|
55
|
+
attr_reader :model, :transactions
|
56
|
+
def initialize(model, &block)
|
57
|
+
@model = model
|
58
|
+
@transactions = []
|
59
|
+
declare_type
|
60
|
+
instance_exec(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def name
|
64
|
+
model.datomic_name
|
65
|
+
end
|
66
|
+
|
67
|
+
def partition
|
68
|
+
model.partition
|
69
|
+
end
|
70
|
+
|
71
|
+
def namespace
|
72
|
+
model.namespace
|
73
|
+
end
|
74
|
+
|
75
|
+
def key(key, subkey=nil)
|
76
|
+
if subkey
|
77
|
+
:"#{namespace}.#{key}/#{subkey}"
|
78
|
+
else
|
79
|
+
:"#{namespace}/#{key}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def declare_type
|
84
|
+
edn [:'db/add', Peer.tempid(kw(partition)), :'db/ident', key(:type, name)]
|
85
|
+
end
|
86
|
+
|
87
|
+
def edn(edn)
|
88
|
+
@transactions << edn
|
89
|
+
end
|
90
|
+
|
91
|
+
def attribute(attr_key, opts={})
|
92
|
+
config = {
|
93
|
+
:'db/id' => opts.fetch(:id) { Peer.tempid(kw('db.part/db')) },
|
94
|
+
:'db/ident' => kw(opts.fetch(:ident) { key(model.datomic_name, attr_key) }),
|
95
|
+
:'db/valueType' => :"db.type/#{opts.fetch(:value_type)}",
|
96
|
+
:'db/cardinality' => :"db.cardinality/#{opts.fetch(:cardinality, :one)}",
|
97
|
+
:'db/doc' => opts.fetch(:doc) { "The #{attr_key} attribute" },
|
98
|
+
:'db.install/_attribute' => :'db.part/db',
|
99
|
+
}
|
100
|
+
|
101
|
+
config[:'db/unique'] = :"db.unique/#{opts[:unique]}" if opts[:unique]
|
102
|
+
|
103
|
+
edn(config)
|
104
|
+
end
|
105
|
+
|
106
|
+
def install!
|
107
|
+
transactions.each do |t|
|
108
|
+
model.transact([t])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Rie
|
2
|
+
class ValidationError < StandardError
|
3
|
+
attr_reader :changes, :errors
|
4
|
+
|
5
|
+
def initialize(changes, errors)
|
6
|
+
@changes = changes
|
7
|
+
@errors = errors
|
8
|
+
end
|
9
|
+
|
10
|
+
def errors_on(key, &b)
|
11
|
+
return enum_for(:errors_on, key).to_a unless block_given?
|
12
|
+
|
13
|
+
errors.each do |(keys, message)|
|
14
|
+
yield message if keys.include? key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def errors_on?(key)
|
19
|
+
errors_on(key).any?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class TransactionValidationError < ValidationError
|
24
|
+
def initialize(changes, datomic_error)
|
25
|
+
@changes = changes
|
26
|
+
@datomic_error = datomic_error
|
27
|
+
end
|
28
|
+
|
29
|
+
def errors
|
30
|
+
# TODO: translate this key
|
31
|
+
[@datomic_error.attribute, @datomic_error.message]
|
32
|
+
end
|
33
|
+
|
34
|
+
def errors_on?(key)
|
35
|
+
changes.model.get_attribute(key).datomic_attribute == @datomic_error.attribute
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Validator
|
40
|
+
# a definition of a validator. the block gets run in the context of
|
41
|
+
# a Scope, and may call `invalid!` with optional attributes and an
|
42
|
+
# error message
|
43
|
+
class Rule
|
44
|
+
class Scope
|
45
|
+
def initialize(attrs, validate, &report)
|
46
|
+
@validate = validate
|
47
|
+
@attrs = attrs
|
48
|
+
@report = report
|
49
|
+
end
|
50
|
+
|
51
|
+
def invalid!(attr_names=nil, description)
|
52
|
+
attr_names ||= @attrs
|
53
|
+
attr_names = Array(attr_names)
|
54
|
+
|
55
|
+
@report.call [attr_names, description]
|
56
|
+
end
|
57
|
+
|
58
|
+
def run(values)
|
59
|
+
instance_exec(*values, &@validate)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(*attrs, &block)
|
64
|
+
@attrs = attrs
|
65
|
+
@block = block
|
66
|
+
end
|
67
|
+
|
68
|
+
def run(changer, &out)
|
69
|
+
values = @attrs.map { |a| changer.send(a) }
|
70
|
+
Scope.new(@attrs, @block, &out).run(values)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :validators
|
75
|
+
def initialize(model, &defn)
|
76
|
+
@model = model
|
77
|
+
@validators = []
|
78
|
+
specify(&defn) if defn
|
79
|
+
end
|
80
|
+
|
81
|
+
def specify(&defn)
|
82
|
+
instance_eval(&defn)
|
83
|
+
end
|
84
|
+
|
85
|
+
# define a validation on *attrs using &block.
|
86
|
+
# See Rule
|
87
|
+
def validate(*attrs, &block)
|
88
|
+
validators << Rule.new(*attrs, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
# returns an enumerable of validation errors on the changeset,
|
92
|
+
# which is empty if the changeset is valid
|
93
|
+
def run_all(changer, &report)
|
94
|
+
return enum_for(:run_all, changer).to_a unless block_given?
|
95
|
+
|
96
|
+
validators.each { |v| v.run(changer, &report) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# raises a ValidationError if the changeset is invalid
|
100
|
+
def run_all!(changer)
|
101
|
+
errors = run_all(changer)
|
102
|
+
raise ValidationError.new(changer, errors) if errors.any?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/rie/version.rb
ADDED
data/rie.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require './lib/rie/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "rie"
|
5
|
+
s.version = Rie.version
|
6
|
+
s.authors = ["Jeanine Adkisson"]
|
7
|
+
s.email = ["jneen@goodguide.com"]
|
8
|
+
s.summary = "A modeling library for datomic"
|
9
|
+
|
10
|
+
s.description = <<-desc.strip.gsub(/\s+/, ' ')
|
11
|
+
Immutable models, first-class changesets, value-based programming
|
12
|
+
desc
|
13
|
+
|
14
|
+
# s.add_dependency 'dalton'
|
15
|
+
|
16
|
+
s.homepage = "https://github.com/GoodGuide/rie"
|
17
|
+
s.rubyforge_project = "rie"
|
18
|
+
s.files = Dir['README.md', 'Gemfile', 'LICENSE', 'rie.gemspec', 'lib/**/*.rb']
|
19
|
+
s.license = 'EPL'
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rie
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeanine Adkisson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Immutable models, first-class changesets, value-based programming
|
14
|
+
email:
|
15
|
+
- jneen@goodguide.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- Gemfile
|
21
|
+
- rie.gemspec
|
22
|
+
- lib/rie.rb
|
23
|
+
- lib/rie/schema.rb
|
24
|
+
- lib/rie/base_changer.rb
|
25
|
+
- lib/rie/validator.rb
|
26
|
+
- lib/rie/attribute.rb
|
27
|
+
- lib/rie/has_database.rb
|
28
|
+
- lib/rie/model.rb
|
29
|
+
- lib/rie/version.rb
|
30
|
+
- lib/rie/base_finder.rb
|
31
|
+
homepage: https://github.com/GoodGuide/rie
|
32
|
+
licenses:
|
33
|
+
- EPL
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project: rie
|
51
|
+
rubygems_version: 2.1.9
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: A modeling library for datomic
|
55
|
+
test_files: []
|
56
|
+
has_rdoc:
|