relaxo-model 0.5.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f93d72c130b887bf10c8e60ed09ab2bc506e6aa
4
- data.tar.gz: 166daf641e55a51bb1d35a202565da55a2b34b5f
3
+ metadata.gz: 85f3d648fadd1ad073b9ba8b19c4bc343022143f
4
+ data.tar.gz: 6da3f03fcdcfcae4a6ada3e1a856649e135c407d
5
5
  SHA512:
6
- metadata.gz: e2df342a27fe0036e569a546c75c2abf090342d0cf1039c3de9194f92bdaa07c6472ab25d298233218626f5c2ce0aef0f1cc1a2bb1a93194eb7cc8341b0706be
7
- data.tar.gz: 9fb5e00b0509388b99345233a76efbdb9f67263ceff30de36ee6eeb954877be0db959def3574e21e95a8553582ca60b4590ee7d3a4ba08b8a4e3522fa167050c
6
+ metadata.gz: 73c64d7d6b81ba525e7348205e400213876f5b7c675f8c299c55497fae3f8eb6125f21fc7297dfb70d721dc598287df73678538a663ae6e5a07a855dba53d778
7
+ data.tar.gz: ab3091e7dd54e8fceec551486522e8822368109b57a684c6c769374ebb9d713135ac51c2014b19c71cd099f0c1fa95145022ac6dfbc9db80f8020efd6605aa30
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.simplecov ADDED
@@ -0,0 +1,9 @@
1
+
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
5
+
6
+ if ENV['TRAVIS']
7
+ require 'coveralls'
8
+ Coveralls.wear!
9
+ end
data/.travis.yml ADDED
@@ -0,0 +1,17 @@
1
+ language: ruby
2
+ sudo: false
3
+ before_install:
4
+ # For testing purposes:
5
+ - git config --global user.name "Samuel Williams"
6
+ - git config --global user.email "samuel@oriontransfer.net"
7
+ rvm:
8
+ - 2.1.8
9
+ - 2.2.4
10
+ - 2.3.1
11
+ - 2.4.0
12
+ - rbx-2
13
+ env: COVERAGE=true
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: ruby-head
17
+ - rvm: "rbx-2"
data/Gemfile CHANGED
@@ -2,3 +2,14 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in relaxo-model.gemspec
4
4
  gemspec
5
+
6
+ gem 'rugged', git: 'git://github.com/libgit2/rugged.git', submodules: true
7
+
8
+ group :development do
9
+ gem "pry"
10
+ end
11
+
12
+ group :test do
13
+ gem 'simplecov'
14
+ gem 'coveralls', require: false
15
+ end
data/README.md CHANGED
@@ -1,64 +1,50 @@
1
1
  # Relaxo Model
2
2
 
3
- Relaxo Model provides a framework for business logic on top of Relaxo/CouchDB. While it supports some traditional ORM style patterns, it is primary focus is to model business processes and logic.
3
+ Relaxo Model provides a framework for business logic on top of Relaxo, a document data store built on top of git. While it supports some traditional relational style patterns, it is primary focus is to model business processes and logic at the document level.
4
+
5
+ [![Build Status](https://secure.travis-ci.org/ioquatix/relaxo-model.svg)](http://travis-ci.org/ioquatix/relaxo-model)
6
+ [![Code Climate](https://codeclimate.com/github/ioquatix/relaxo-model.svg)](https://codeclimate.com/github/ioquatix/relaxo-model)
7
+ [![Coverage Status](https://coveralls.io/repos/ioquatix/relaxo-model/badge.svg)](https://coveralls.io/r/ioquatix/relaxo-model)
4
8
 
5
9
  ## Basic Usage
6
10
 
7
11
  Here is a simple example of a traditional ORM style model:
8
12
 
9
- require 'relaxo'
10
13
  require 'relaxo/model'
11
14
 
12
- database = Relaxo.connect("http://localhost:5984/test")
15
+ database = Relaxo.connect("test")
13
16
 
14
17
  trees = [
15
- {:name => 'Hinoki', :planted => Date.parse("1948/4/2")},
16
- {:name => 'Rimu', :planted => Date.parse("1962/8/7")}
18
+ {:name => 'Hinoki', :planted => Date.parse("2013/11/17")},
19
+ {:name => 'Keyaki', :planted => Date.parse("2016/9/24")}
17
20
  ]
18
-
21
+
19
22
  class Tree
20
23
  include Relaxo::Model
21
-
24
+
25
+ property :id, UUID
22
26
  property :name
23
27
  property :planted, Attribute[Date]
24
-
25
- # Ensure you've loaded an appropriate design document:
26
- view :all, 'catalog/tree', Tree
28
+
29
+ view :all, [:type], index: [:id]
27
30
  end
28
31
 
29
- trees.each do |doc|
30
- tree = Tree.create(database, doc)
31
-
32
- tree.save
32
+ database.commit(message: "Create trees") do |changeset|
33
+ trees.each do |tree|
34
+ Tree.insert(changeset, tree)
35
+ end
33
36
  end
34
37
 
35
- Tree.all(database).each do |tree|
36
- puts "A #{tree.name} was planted on #{tree.planted.to_s}."
38
+ database.current do |dataset|
39
+ Tree.all(dataset).each do |tree|
40
+ puts "A #{tree.name} was planted on #{tree.planted.to_s}."
37
41
 
38
- # Expected output:
39
- # => A Rimu was planted on 1962-08-07.
40
- # => A Hinoki was planted on 1948-04-02.
41
-
42
- tree.delete
42
+ # Expected output:
43
+ # A Hinoki was planted on 2013-11-17.
44
+ # A Keyaki was planted on 2016-09-24.
45
+ end
43
46
  end
44
-
45
- Here is the design document:
46
-
47
- - _id: "_design/catalog"
48
- language: javascript
49
- views:
50
- tree:
51
- map: |
52
- function(doc) {
53
- if (doc.type == 'tree') {
54
- emit(doc._id, doc._rev);
55
- }
56
- }
57
-
58
- If the design document was saved as `catalog.yaml`, you could load it using relaxo into the `test` database as follows:
59
-
60
- relaxo test catalog.yaml
61
-
47
+
62
48
  ## Contributing
63
49
 
64
50
  1. Fork it
@@ -71,7 +57,7 @@ If the design document was saved as `catalog.yaml`, you could load it using rela
71
57
 
72
58
  Released under the MIT license.
73
59
 
74
- Copyright, 2010-2013, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
60
+ Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
75
61
 
76
62
  Permission is hereby granted, free of charge, to any person obtaining a copy
77
63
  of this software and associated documentation files (the "Software"), to deal
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |task|
5
+ task.rspec_opts = ["--require", "simplecov"] if ENV['COVERAGE']
6
+ end
7
+
8
+ task :default => :spec
9
+
10
+ task :console do
11
+ require 'pry'
12
+
13
+ require_relative 'lib/relaxo/model'
14
+
15
+ DB = Relaxo.connect(File.join(__dir__, 'testdb'))
16
+
17
+ Pry.start
18
+ end
@@ -18,22 +18,48 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'relaxo/model/recordset'
21
+ require_relative 'recordset'
22
22
 
23
23
  module Relaxo
24
24
  module Model
25
+ Key = Struct.new(:prefix, :index) do
26
+ def resolve(key_path, model, **arguments)
27
+ key_path.collect do |component|
28
+ case component
29
+ when Symbol
30
+ arguments[component] || model.send(component) || "null"
31
+ when Array
32
+ resolve(component, model, arguments).join('-')
33
+ when Proc
34
+ model.instance_exec(**arguments, &component)
35
+ else
36
+ component
37
+ end
38
+ end
39
+ end
40
+
41
+ def object_path(model, **arguments)
42
+ resolve(self.prefix + self.index, model, **arguments).join('/')
43
+ end
44
+
45
+ def prefix_path(model, **arguments)
46
+ resolve(self.prefix, model, **arguments).join('/')
47
+ end
48
+ end
49
+
25
50
  module Base
26
51
  def self.extended(child)
27
52
  # $stderr.puts "#{self} extended -> #{child} (setup Base)"
28
53
  child.instance_variable_set(:@properties, {})
29
54
  child.instance_variable_set(:@relationships, {})
30
-
55
+
56
+ child.instance_variable_set(:@keys, {})
57
+ child.instance_variable_set(:@primary_key, nil)
58
+
31
59
  default_type = child.name.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase!
32
60
  child.instance_variable_set(:@type, default_type)
33
61
  end
34
62
 
35
- attr :type
36
-
37
63
  def metaclass
38
64
  class << self; self; end
39
65
  end
@@ -41,61 +67,36 @@ module Relaxo
41
67
  attr :type
42
68
  attr :properties
43
69
  attr :relationships
44
-
45
- DEFAULT_VIEW_OPTIONS = {:include_docs => true}
46
-
47
- def view(name, path, *args)
48
- options = Hash === args.last ? args.pop : DEFAULT_VIEW_OPTIONS
49
- klass = args.pop || options[:class]
50
-
51
- self.metaclass.send(:define_method, name) do |database, query = {}|
52
- records = database.view(path, query.merge(options))
53
- Recordset.new(database, records, klass)
54
- end
70
+
71
+ attr :keys
72
+ attr :primary_key
73
+
74
+ def parent_type klass
75
+ @type = [klass.type, self.type].join('/')
55
76
  end
56
-
57
- DEFAULT_RELATIONSHIP_OPTIONS = {
58
- :key => lambda {|object, query| query[:key] = object.id},
59
- :include_docs => true
60
- }
61
-
62
- def relationship(name, path, *args, &block)
63
- options = Hash === args.last ? args.pop : DEFAULT_RELATIONSHIP_OPTIONS
64
- klass = block || args.pop || options[:class]
65
-
66
- @relationships[name] = options
67
-
68
- # This reduction returns a single result, so just provide the first row directly:
69
- reduction = options.delete(:reduction)
70
-
71
- options = options.dup
72
-
73
- update_key_function(options, :key)
74
- update_key_function(options, :startkey)
75
- update_key_function(options, :endkey)
76
-
77
- self.send(:define_method, name) do |query = {}|
78
- query = query.merge(options)
79
-
80
- [:key, :startkey, :endkey].each do |name|
81
- if options[name].respond_to? :call
82
- options[name].call(self, query)
83
- end
84
- end
85
-
86
- recordset = Recordset.new(@database, @database.view(path, query), klass)
87
-
88
- if reduction == :first
89
- recordset.first
90
- else
91
- recordset
92
- end
77
+
78
+ def view(name, path = nil, klass: self, index: nil)
79
+ key = Key.new(path, index)
80
+
81
+ if index
82
+ @keys[name] = key
83
+ @primary_key ||= key
84
+ end
85
+
86
+ self.metaclass.send(:define_method, name) do |dataset, **arguments|
87
+ Recordset.new(dataset, key.prefix_path(self, **arguments), klass)
88
+ end
89
+
90
+ self.metaclass.send(:define_method, "fetch_#{name}") do |dataset, **arguments|
91
+ self.fetch(dataset, key.object_path(self, **arguments))
92
+ end
93
+
94
+ self.send(:define_method, name) do |**arguments|
95
+ Recordset.new(self.dataset, key.object_path(self, **arguments), self.class)
93
96
  end
94
97
  end
95
-
98
+
96
99
  def property(name, klass = nil)
97
- name = name.to_s
98
-
99
100
  @properties[name] = klass
100
101
 
101
102
  self.send(:define_method, name) do
@@ -105,7 +106,7 @@ module Relaxo
105
106
  if klass
106
107
  value = @attributes[name]
107
108
 
108
- @changed[name] = klass.convert_from_primative(@database, value)
109
+ @changed[name] = klass.convert_from_primative(dataset, value)
109
110
  else
110
111
  @changed[name] = @attributes[name]
111
112
  end
@@ -130,26 +131,6 @@ module Relaxo
130
131
  end
131
132
  end
132
133
  end
133
-
134
- private
135
-
136
- # Used for generating key functions for relationships - subject to change so private for now.
137
- def update_key_function(options, name)
138
- key = options[name]
139
-
140
- if key == :self
141
- options[name] = lambda do |object, query|
142
- query[name] = object.id
143
- end
144
- elsif Array === key
145
- index = key.index(:self)
146
-
147
- options[name] = lambda do |object, query|
148
- query[name] = key.dup
149
- query[name][index] = object.id
150
- end
151
- end
152
- end
153
134
  end
154
135
  end
155
136
  end
@@ -28,15 +28,30 @@ module Relaxo
28
28
  child.send(:extend, Base)
29
29
  end
30
30
 
31
- def initialize(database, attributes = {})
32
- # Raw key-value database
31
+ def initialize(dataset, object = nil, changed = {}, **attributes)
32
+ @dataset = dataset
33
+ @object = object
34
+ @changed = changed
33
35
  @attributes = attributes
34
- @database = database
35
- @changed = {}
36
+ end
37
+
38
+ def load_object
39
+ if @object
40
+ attributes = MessagePack.load(@object.data, symbolize_keys: true)
41
+
42
+ # We prefer existing @attributes over ones loaded from data. This allows the API to load from an object, but specify new attributes.
43
+ @attributes = attributes.merge(@attributes)
44
+ end
45
+ end
46
+
47
+ def dump
48
+ flatten!
49
+
50
+ MessagePack.dump(@attributes)
36
51
  end
37
52
 
38
53
  attr :attributes
39
- attr :database
54
+ attr :dataset
40
55
  attr :changed
41
56
 
42
57
  def clear(key)
@@ -48,19 +63,19 @@ module Relaxo
48
63
  enumerator = primative_attributes
49
64
 
50
65
  if only == :all
51
- enumerator = enumerator.select{|key, value| self.class.properties.include? key.to_s}
66
+ enumerator = enumerator.select{|key, value| self.class.properties.include? key.to_sym}
52
67
  elsif only.respond_to? :include?
53
68
  enumerator = enumerator.select{|key, value| only.include? key.to_sym}
54
69
  end
55
70
 
56
71
  enumerator.each do |key, value|
57
- key = key.to_s
72
+ key = key.to_sym
58
73
 
59
74
  klass = self.class.properties[key]
60
75
 
61
76
  if klass
62
77
  # This might raise a validation error
63
- value = klass.convert_from_primative(@database, value)
78
+ value = klass.convert_from_primative(@dataset, value)
64
79
  end
65
80
 
66
81
  self[key] = value
@@ -70,8 +85,6 @@ module Relaxo
70
85
  end
71
86
 
72
87
  def [] name
73
- name = name.to_s
74
-
75
88
  if self.class.properties.include? name
76
89
  self.send(name)
77
90
  else
@@ -80,8 +93,6 @@ module Relaxo
80
93
  end
81
94
 
82
95
  def []= name, value
83
- name = name.to_s
84
-
85
96
  if self.class.properties.include? name
86
97
  self.send("#{name}=", value)
87
98
  else
@@ -96,16 +107,18 @@ module Relaxo
96
107
  def flatten!
97
108
  # Flatten changed properties:
98
109
  self.class.properties.each do |key, klass|
99
- if @changed.include? key
110
+ if @changed.include?(key)
100
111
  if klass
101
112
  @attributes[key] = klass.convert_to_primative(@changed.delete(key))
102
113
  else
103
114
  @attributes[key] = @changed.delete(key)
104
115
  end
116
+ elsif !@attributes.include?(key) and klass.respond_to?(:default)
117
+ @attributes[key] = klass.default
105
118
  end
106
119
  end
107
120
 
108
- # Non-specific properties, serialised by JSON:
121
+ # Non-specific properties:
109
122
  @changed.each do |key, value|
110
123
  @attributes[key] = value
111
124
  end
@@ -20,6 +20,8 @@
20
20
 
21
21
  require 'relaxo/model/component'
22
22
 
23
+ require 'msgpack'
24
+
23
25
  module Relaxo
24
26
  module Model
25
27
  class ValidationError < StandardError
@@ -45,8 +47,6 @@ module Relaxo
45
47
  end
46
48
 
47
49
  module Document
48
- TYPE = 'type'
49
-
50
50
  def self.included(child)
51
51
  child.send(:include, Component)
52
52
  child.send(:extend, ClassMethods)
@@ -54,8 +54,8 @@ module Relaxo
54
54
 
55
55
  module ClassMethods
56
56
  # Create a new document with a particular specified type.
57
- def create(database, properties = nil)
58
- instance = self.new(database, {TYPE => @type})
57
+ def create(dataset, properties = nil)
58
+ instance = self.new(dataset, type: @type)
59
59
 
60
60
  if properties
61
61
  properties.each do |key, value|
@@ -67,48 +67,45 @@ module Relaxo
67
67
 
68
68
  return instance
69
69
  end
70
-
70
+
71
+ def insert(dataset, properties)
72
+ instance = self.create(dataset, properties)
73
+
74
+ instance.save(dataset)
75
+
76
+ return instance
77
+ end
78
+
71
79
  # Fetch a record or create a model object from a hash of attributes.
72
- def fetch(database, id_or_attributes)
73
- if Hash === id_or_attributes
74
- instance = self.new(database, id_or_attributes)
75
- else
76
- instance = self.new(database, database.get(id_or_attributes).to_hash)
80
+ def fetch(dataset, path = nil, **attributes)
81
+ if path and object = dataset.read(path)
82
+ instance = self.new(dataset, object, attributes)
83
+
84
+ instance.load_object
85
+
86
+ instance.after_fetch
87
+
88
+ return instance
77
89
  end
78
-
79
- instance.after_fetch
80
-
81
- return instance
82
90
  end
83
91
  end
84
92
 
85
93
  include Comparable
86
94
 
87
- def id
88
- @attributes[ID]
89
- end
90
-
91
- # A string suitable for use as a URL parameter.
92
- alias to_param id
93
-
94
95
  def new_record?
95
- !saved?
96
+ !persisted?
96
97
  end
97
98
 
98
- def saved?
99
- @attributes.key? ID
99
+ def persisted?
100
+ @object != nil
100
101
  end
101
102
 
102
103
  def changed? key
103
104
  @changed.include? key.to_s
104
105
  end
105
106
 
106
- def rev
107
- @attributes[REV]
108
- end
109
-
110
107
  def type
111
- @attributes[TYPE]
108
+ @attributes[:type]
112
109
  end
113
110
 
114
111
  def valid_type?
@@ -122,54 +119,96 @@ module Relaxo
122
119
  def after_save
123
120
  end
124
121
 
125
- # Duplicate the model object, and possibly change the database it is connected to. You will potentially have two objects referring to the same record.
126
- def dup(database = @database)
127
- clone = self.class.new(database, @attributes.dup)
122
+ def to_s
123
+ if primary_key = self.class.primary_key
124
+ primary_key.object_path(self)
125
+ else
126
+ super
127
+ end
128
+ end
129
+
130
+ # Duplicate the model object, and possibly change the dataset it is connected to. You will potentially have two objects referring to the same record.
131
+ def dup
132
+ clone = self.class.new(@dataset, @object, @changed, **@attributes.dup)
128
133
 
129
134
  clone.after_fetch
130
135
 
131
136
  return clone
132
137
  end
138
+
139
+ def paths
140
+ return to_enum(:paths) unless block_given?
141
+
142
+ self.class.keys.each do |name, key|
143
+ # @attributes is not modified until we call self.dump (which flattens @attributes into @changes). When we generate paths, we want to ensure these are done based on the non-mutable state of the object.
144
+ yield key.object_path(self, @attributes)
145
+ end
146
+ end
133
147
 
134
148
  # Save the model object.
135
- def save
136
- return if saved? and @changed.empty?
149
+ def save(dataset)
150
+ return if persisted? and @changed.empty?
137
151
 
138
152
  before_save
139
-
153
+
140
154
  if errors = self.validate
141
155
  return errors
142
156
  end
143
157
 
144
- self.flatten!
145
-
146
- @database.save(@attributes)
147
-
158
+ existing_paths = persisted? ? paths.to_a : []
159
+
160
+ # Write data, check if any actual changes made:
161
+ object = dataset.append(self.dump)
162
+ return if object == @object
163
+
164
+ existing_paths.each do |path|
165
+ dataset.delete(path)
166
+ end
167
+
168
+ paths do |path|
169
+ if dataset.exist?(path)
170
+ raise KeyError, "Dataset already contains path: #{path}, when inserting #{@attributes.inspect}"
171
+ end
172
+
173
+ dataset.write(path, object)
174
+ end
175
+
176
+ @dataset = dataset
177
+ @object = object
178
+
148
179
  after_save
149
180
 
150
181
  return true
151
182
  end
152
183
 
153
- def save!
154
- result = self.save
184
+ def save!(dataset)
185
+ result = self.save(dataset)
155
186
 
156
187
  if result != true
157
- throw ValidationErrors.new(result)
188
+ raise ValidationErrors.new(result)
158
189
  end
159
190
 
160
191
  return self
161
192
  end
162
193
 
194
+ def reload(dataset)
195
+ @dataset = dataset
196
+ end
197
+
163
198
  def before_delete
164
199
  end
165
200
 
166
201
  def after_delete
167
202
  end
168
203
 
169
- def delete
204
+ def delete(dataset)
170
205
  before_delete
171
-
172
- @database.delete(@attributes)
206
+
207
+ @changed.clear
208
+
209
+ paths.each do |path|
210
+ @dataset.delete(path)
211
+ end
173
212
 
174
213
  after_delete
175
214
  end
@@ -31,7 +31,7 @@ module Relaxo
31
31
  @@attributes[klass] = Proc.new(&block)
32
32
  end
33
33
 
34
- def self.[] (klass, proc = nil)
34
+ def self.[](klass, proc = nil)
35
35
  self.new(klass, &proc)
36
36
  end
37
37
 
@@ -39,15 +39,15 @@ module Relaxo
39
39
  @klass = klass
40
40
 
41
41
  if block_given?
42
- self.instance_eval &serialization
42
+ self.instance_eval(&serialization)
43
43
  else
44
- self.instance_eval &@@attributes[klass]
44
+ self.instance_eval(&@@attributes[klass])
45
45
  end
46
46
  end
47
47
  end
48
48
 
49
49
  class Serialized
50
- def self.[] (klass, proc = nil)
50
+ def self.[](klass, proc = nil)
51
51
  self.new(klass, &proc)
52
52
  end
53
53
 
@@ -59,7 +59,7 @@ module Relaxo
59
59
  @klass.dump(value)
60
60
  end
61
61
 
62
- def convert_from_primative(database, value)
62
+ def convert_from_primative(dataset, value)
63
63
  @klass.load(value)
64
64
  end
65
65
  end
@@ -83,11 +83,11 @@ module Relaxo
83
83
  end
84
84
  end
85
85
 
86
- def convert_from_primative(database, value)
86
+ def convert_from_primative(dataset, value)
87
87
  if value.nil? or value.empty?
88
88
  nil
89
89
  else
90
- @klass.convert_from_primative(database, value)
90
+ @klass.convert_from_primative(dataset, value)
91
91
  end
92
92
  end
93
93
  end
@@ -100,7 +100,7 @@ module Relaxo
100
100
  value ? true : false
101
101
  end
102
102
 
103
- def convert_from_primative(database, value)
103
+ def convert_from_primative(dataset, value)
104
104
  [true, "on", "true"].include?(value)
105
105
  end
106
106
  end
@@ -110,7 +110,7 @@ module Relaxo
110
110
  value.to_i
111
111
  end
112
112
 
113
- def convert_from_primative(database, value)
113
+ def convert_from_primative(dataset, value)
114
114
  value.to_i
115
115
  end
116
116
  end
@@ -120,7 +120,7 @@ module Relaxo
120
120
  value.to_f
121
121
  end
122
122
 
123
- def convert_from_primative(database, value)
123
+ def convert_from_primative(dataset, value)
124
124
  value.to_f
125
125
  end
126
126
  end
@@ -130,7 +130,7 @@ module Relaxo
130
130
  value.iso8601
131
131
  end
132
132
 
133
- def convert_from_primative(database, value)
133
+ def convert_from_primative(dataset, value)
134
134
  Date.parse(value)
135
135
  end
136
136
  end
@@ -140,7 +140,7 @@ module Relaxo
140
140
  value.iso8601
141
141
  end
142
142
 
143
- def convert_from_primative(database, value)
143
+ def convert_from_primative(dataset, value)
144
144
  DateTime.parse(value)
145
145
  end
146
146
  end
@@ -150,7 +150,7 @@ module Relaxo
150
150
  value.to_s
151
151
  end
152
152
 
153
- def convert_from_primative(database, value)
153
+ def convert_from_primative(dataset, value)
154
154
  value.to_s
155
155
  end
156
156
  end
@@ -28,7 +28,7 @@ module Relaxo
28
28
  [value.salt, value.checksum]
29
29
  end
30
30
 
31
- def convert_from_primative(database, value)
31
+ def convert_from_primative(dataset, value)
32
32
  if String === value
33
33
  # If the primative value is a string, we are saving the password:
34
34
  BCrypt::Password.create(value)
@@ -28,7 +28,7 @@ module Relaxo
28
28
  value.to_s('F')
29
29
  end
30
30
 
31
- def convert_from_primative(database, value)
31
+ def convert_from_primative(dataset, value)
32
32
  value.to_d
33
33
  end
34
34
  end
@@ -44,27 +44,18 @@ module Relaxo
44
44
  @lookup[type]
45
45
  end
46
46
 
47
- def convert_to_primative(object)
48
- unless object.saved?
49
- object.save
50
- end
47
+ def convert_to_primative(document)
48
+ raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
51
49
 
52
- [object.type, object.id]
50
+ document.paths.first
53
51
  end
54
52
 
55
- def convert_from_primative(database, reference)
56
- # Legacy support for old polymorphic types - to remove.
57
- if Array === reference
58
- type, id = reference
59
- else
60
- id = reference
61
- end
53
+ def convert_from_primative(dataset, path)
54
+ type, _, _ = path.rpartition('/')
62
55
 
63
- attributes = database.get(id).to_hash
56
+ klass = lookup(type)
64
57
 
65
- klass = lookup(attributes['type'])
66
-
67
- klass.fetch(database, attributes)
58
+ klass.fetch(dataset, path)
68
59
  end
69
60
  end
70
61
 
@@ -81,39 +72,37 @@ module Relaxo
81
72
  @klass = klass
82
73
  end
83
74
 
84
- def convert_to_primative(object)
85
- unless object.saved?
86
- object.save
87
- end
88
-
89
- object.id
75
+ def convert_to_primative(document)
76
+ raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
77
+
78
+ document.paths.first
90
79
  end
91
80
 
92
- def convert_from_primative(database, id)
93
- @klass.fetch(database, id)
81
+ def convert_from_primative(dataset, path)
82
+ @klass.fetch(dataset, path)
94
83
  end
95
84
  end
96
-
85
+
97
86
  class HasOne < BelongsTo
98
87
  end
99
88
 
100
89
  class HasMany < HasOne
101
- def convert_to_primative(value)
102
- value.each do |document|
103
- document.save unless document.saved?
90
+ def convert_to_primative(documents)
91
+ documents.each do |document|
92
+ raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
104
93
  end
105
-
106
- value.collect{|document| document.id}
94
+
95
+ documents.collect{|document| document.paths.first}
107
96
  end
108
97
 
109
- def convert_from_primative(database, value)
110
- value.collect{|id| @klass.fetch(database, id)}
98
+ def convert_from_primative(dataset, value)
99
+ value.collect{|id| @klass.fetch(dataset, id)}
111
100
  end
112
101
  end
113
102
 
114
103
  # Returns the raw value, typically used for reductions:
115
104
  module ValueOf
116
- def self.new(database, value)
105
+ def self.new(dataset, value)
117
106
  value
118
107
  end
119
108
  end
@@ -133,9 +122,9 @@ module Relaxo
133
122
  end
134
123
  end
135
124
 
136
- def convert_from_primative(database, value)
125
+ def convert_from_primative(dataset, value)
137
126
  value.collect do |item|
138
- @klass.convert_from_primative(database, item)
127
+ @klass.convert_from_primative(dataset, item)
139
128
  end
140
129
  end
141
130
  end
@@ -28,7 +28,7 @@ module Relaxo
28
28
  [value.amount.to_s('F'), value.name]
29
29
  end
30
30
 
31
- def convert_from_primative(database, value)
31
+ def convert_from_primative(dataset, value)
32
32
  if Array === value
33
33
  @klass.new(value[0], value[1])
34
34
  else
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -18,30 +18,22 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'relaxo/attachments'
22
- require 'relaxo/model/component'
21
+ require 'securerandom'
23
22
 
24
23
  module Relaxo
25
24
  module Model
26
- module Component
27
- ATTACHMENTS = '_attachments'
28
- DEFAULT_ATTACHMENT_CONTENT_TYPE = 'application/octet-stream'
29
-
30
- # Attach a file to the document with a given path.
31
- def attach(path, data, options = {})
32
- options[:content_type] ||= DEFAULT_ATTACHMENT_CONTENT_TYPE
25
+ module Properties
26
+ module UUID
27
+ def self.default
28
+ SecureRandom.uuid
29
+ end
33
30
 
34
- @database.attach(@attributes, path, data, options)
35
- end
36
-
37
- # Get all attachments, optionally filtering with a particular prefix path.
38
- def attachments(prefix = nil)
39
- all_attachments = (@attributes[ATTACHMENTS] || [])
31
+ def self.convert_to_primative(value)
32
+ value
33
+ end
40
34
 
41
- if prefix
42
- all_attachments.select{|name, attachment| name.start_with? prefix}
43
- else
44
- all_attachments
35
+ def self.convert_from_primative(dataset, value)
36
+ value
45
37
  end
46
38
  end
47
39
  end
@@ -20,3 +20,4 @@
20
20
 
21
21
  require 'relaxo/model/properties/attribute'
22
22
  require 'relaxo/model/properties/composite'
23
+ require 'relaxo/model/properties/uuid'
@@ -1,5 +1,4 @@
1
-
2
- # Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
3
2
  #
4
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -24,47 +23,28 @@ module Relaxo
24
23
  class Recordset
25
24
  include Enumerable
26
25
 
27
- def initialize(database, view, klass = nil)
28
- @database = database
29
- @view = view
30
-
31
- @klass = klass
26
+ def initialize(dataset, path, model)
27
+ @dataset = dataset
28
+ @path = path
29
+ @model = model
32
30
  end
33
31
 
34
32
  attr :klass
35
33
  attr :database
36
34
  attr :view
37
35
 
38
- def count
39
- rows.count
40
- end
41
-
42
- def offset
43
- @view["offset"]
36
+ def empty?
37
+ !@dataset.each(@path).any?
44
38
  end
45
-
46
- def rows
47
- @view["rows"]
48
- end
49
-
50
- def each(klass = nil, &block)
51
- klass ||= @klass
52
-
53
- if klass
54
- if klass.respond_to? :call
55
- rows.each do |row|
56
- yield klass.call(@database, row)
57
- end
58
- else
59
- rows.each do |row|
60
- # If user specified :include_docs => true, row['doc'] contains the primary value:
61
- yield klass.new(@database, row['doc'] || row['value'])
62
- end
63
- end
64
- else
65
- rows.each &block
39
+
40
+ def each(model = @model, &block)
41
+ @dataset.each(@path) do |name, object|
42
+ object = model.new(@dataset, object)
43
+ object.load_object
44
+
45
+ yield object
66
46
  end
67
47
  end
68
48
  end
69
49
  end
70
- end
50
+ end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Relaxo
22
22
  module Model
23
- VERSION = "0.5.1"
23
+ VERSION = "0.9.0"
24
24
  end
25
25
  end
data/lib/relaxo/model.rb CHANGED
@@ -22,7 +22,6 @@ require 'relaxo'
22
22
 
23
23
  require 'relaxo/model/document'
24
24
  require 'relaxo/model/properties'
25
- require 'relaxo/model/recordset'
26
25
 
27
26
  module Relaxo
28
27
  module Model
data/relaxo-model.gemspec CHANGED
@@ -22,7 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency("relaxo", "~> 0.4.0")
25
+ spec.add_dependency("relaxo", "~> 1.0")
26
+ spec.add_dependency("msgpack", "~> 1.0")
26
27
 
27
28
  spec.add_development_dependency "bundler", "~> 1.3"
28
29
  spec.add_development_dependency "rspec", "~> 3.4.0"
@@ -0,0 +1,104 @@
1
+
2
+ require 'relaxo/model'
3
+
4
+ class Invoice
5
+ class Transaction; end
6
+
7
+ include Relaxo::Model
8
+
9
+ property :id, UUID
10
+ property :number
11
+ property :name
12
+
13
+ property :date, Attribute[Date]
14
+
15
+ view :all, [:type], index: [:id]
16
+
17
+ def transactions
18
+ Invoice::Transaction.by_invoice(@dataset, invoice: self)
19
+ end
20
+ end
21
+
22
+ class Invoice::Transaction
23
+ include Relaxo::Model
24
+
25
+ parent_type Invoice
26
+
27
+ property :id, UUID
28
+ property :invoice, BelongsTo[Invoice]
29
+ property :date, Attribute[Date]
30
+
31
+ view :all, [:type], index: [:id]
32
+
33
+ view :by_invoice, [:type, 'by_invoice', :invoice], index: [[:date, :id]]
34
+ end
35
+
36
+ RSpec.describe Relaxo::Model::Document do
37
+ let(:database_path) {File.join(__dir__, 'test')}
38
+ let(:database) {Relaxo.connect(database_path)}
39
+
40
+ let(:document_path) {'test/document.json'}
41
+
42
+ before(:each) {FileUtils.rm_rf(database_path)}
43
+
44
+ it "should create and save document" do
45
+ model = Invoice.create(database.current,
46
+ name: "Software Development"
47
+ )
48
+
49
+ expect(model.persisted?).to be_falsey
50
+
51
+ database.commit(message: "Adding test model") do |dataset|
52
+ model.save(dataset)
53
+ end
54
+
55
+ expect(model.id).to_not be nil
56
+ expect(model.persisted?).to be_truthy
57
+ end
58
+
59
+ it "should enumerate model objects" do
60
+ database.commit(message: "Adding test model") do |dataset|
61
+ Invoice.insert(dataset, name: "Software Development")
62
+ Invoice.insert(dataset, name: "Website Hosting")
63
+ Invoice.insert(dataset, name: "Backup Services")
64
+ end
65
+
66
+ expect(Invoice.all(database.current).count).to be == 3
67
+ end
68
+
69
+ it "should create model indexes" do
70
+ database.commit(message: "Adding test model") do |dataset|
71
+ invoice = Invoice.create(dataset, name: "Software Development")
72
+ invoice.save(dataset)
73
+
74
+ transaction = Invoice::Transaction.create(dataset, date: Date.today, invoice: invoice)
75
+ transaction.save(dataset)
76
+ end
77
+
78
+ expect(Invoice.all(database.current).count).to be == 1
79
+ expect(Invoice::Transaction.all(database.current).count).to be == 1
80
+
81
+ invoice = Invoice.all(database.current).first
82
+ expect(invoice).to_not be nil
83
+
84
+ transactions = Invoice::Transaction.by_invoice(database.current, invoice: invoice)
85
+ expect(transactions).to_not be_empty
86
+ end
87
+
88
+ it "updates indexes correctly" do
89
+ transaction = nil
90
+
91
+ database.commit(message: "Adding test model") do |dataset|
92
+ invoice = Invoice.create(dataset, name: "Software Development")
93
+ invoice.save(dataset)
94
+
95
+ transaction = Invoice::Transaction.create(dataset, date: Date.today, invoice: invoice)
96
+ transaction.save(dataset)
97
+ end
98
+
99
+ database.commit(message: "Adding test model") do |dataset|
100
+ transaction.date = Date.today - 1
101
+ transaction.save(dataset)
102
+ end
103
+ end
104
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaxo-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-24 00:00:00.000000000 Z
11
+ date: 2017-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: relaxo
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.4.0
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.4.0
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: msgpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -75,10 +89,13 @@ executables: []
75
89
  extensions: []
76
90
  extra_rdoc_files: []
77
91
  files:
92
+ - ".gitignore"
93
+ - ".simplecov"
94
+ - ".travis.yml"
78
95
  - Gemfile
79
96
  - README.md
97
+ - Rakefile
80
98
  - lib/relaxo/model.rb
81
- - lib/relaxo/model/attachments.rb
82
99
  - lib/relaxo/model/base.rb
83
100
  - lib/relaxo/model/component.rb
84
101
  - lib/relaxo/model/document.rb
@@ -88,10 +105,11 @@ files:
88
105
  - lib/relaxo/model/properties/bigdecimal.rb
89
106
  - lib/relaxo/model/properties/composite.rb
90
107
  - lib/relaxo/model/properties/latinum.rb
108
+ - lib/relaxo/model/properties/uuid.rb
91
109
  - lib/relaxo/model/recordset.rb
92
110
  - lib/relaxo/model/version.rb
93
- - rakefile.rb
94
111
  - relaxo-model.gemspec
112
+ - spec/relaxo/model/document_spec.rb
95
113
  homepage: http://www.codeotaku.com/projects/relaxo/model
96
114
  licenses:
97
115
  - MIT
@@ -116,4 +134,5 @@ rubygems_version: 2.6.10
116
134
  signing_key:
117
135
  specification_version: 4
118
136
  summary: A model layer for CouchDB with minimal global state.
119
- test_files: []
137
+ test_files:
138
+ - spec/relaxo/model/document_spec.rb
data/rakefile.rb DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"