rie 0.0.2
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.
- 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:
|