ixtlan-datamapper 0.1.0
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 +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +94 -0
- data/Rakefile +29 -0
- data/ixtlan-datamapper.gemspec +32 -0
- data/lib/ixtlan-datamapper.rb +23 -0
- data/lib/ixtlan/datamapper/collection.rb +28 -0
- data/lib/ixtlan/datamapper/collection.rb~ +28 -0
- data/lib/ixtlan/datamapper/conditional_get.rb +46 -0
- data/lib/ixtlan/datamapper/conditional_get.rb~ +47 -0
- data/lib/ixtlan/datamapper/cuba_plugin.rb~ +74 -0
- data/lib/ixtlan/datamapper/immutable.rb +18 -0
- data/lib/ixtlan/datamapper/immutable.rb~ +83 -0
- data/lib/ixtlan/datamapper/modified.rb~ +83 -0
- data/lib/ixtlan/datamapper/modified_by.rb +39 -0
- data/lib/ixtlan/datamapper/modified_by.rb~ +39 -0
- data/lib/ixtlan/datamapper/optimistic.rb~ +53 -0
- data/lib/ixtlan/datamapper/optimistic_get.rb +71 -0
- data/lib/ixtlan/datamapper/optimistic_get.rb~ +72 -0
- data/lib/ixtlan/datamapper/stale_check.rb~ +49 -0
- data/lib/ixtlan/datamapper/stale_object_exception.rb +26 -0
- data/lib/ixtlan/datamapper/stale_object_exception.rb~ +26 -0
- data/lib/ixtlan/datamapper/use_utc.rb +4 -0
- data/lib/ixtlan/datamapper/use_utc.rb~ +5 -0
- data/lib/ixtlan/datamapper/validations_ext.rb +61 -0
- data/spec/collection_spec.rb +57 -0
- data/spec/collection_spec.rb~ +54 -0
- data/spec/conditional_get_spec.rb +69 -0
- data/spec/conditional_get_spec.rb~ +70 -0
- data/spec/datamapper_spec.rb~ +74 -0
- data/spec/immutable_spec.rb +33 -0
- data/spec/immutable_spec.rb~ +70 -0
- data/spec/modified_by_spec.rb +54 -0
- data/spec/modified_by_spec.rb~ +33 -0
- data/spec/optimistic_get_spec.rb +57 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/spec_helper.rb~ +12 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b50625c5e23298cea7e2f56ab39f7eac2cdd2150
|
4
|
+
data.tar.gz: b98eaa3f4b9ac7eb6a78172617e93bf92db76e2f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2fdb5f154e80bd69681295d723ac9af2c7ba05de096ea2c7b057c66a676f9a9f3671353ba40e55250df14ede9614b33a15dfc11872ec35fc188fbf7d8ef1739e
|
7
|
+
data.tar.gz: 5cadca5c14114b50e59d58c20c74c3a69d749245e630aadbb9801a18787ff51e002f02dd7a237e7c144d012c4076d34f6314aa411bfccd6fbf8e839513fff00f
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Kristian Meier
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Ixtlan DataMapper #
|
2
|
+
|
3
|
+
* [](http://travis-ci.org/mkristian/ixtlan-datamapper)
|
4
|
+
* [](https://gemnasium.com/mkristian/ixtlan-datamapper)
|
5
|
+
* [](https://codeclimate.com/github/mkristian/ixtlan-datamapper)
|
6
|
+
|
7
|
+
|
8
|
+
it adds optimistic persistence support to DataMapper and ActveRecord using the updated\_at property/attribute which is automatically updated on any change of the model (for datamapper you need dm-timestamps for that). to load a model use `optimistic_get`/`optimistic_get!`/`optimistic_find` respectively where the first argument is the last `updated_at` value which the client has. if the client data is uptodate then the `optimistic_XYZ` method will return the database entity otherwise raise an exception or return nil respectively.
|
9
|
+
|
10
|
+
|
11
|
+
## optimistic/conditional get ##
|
12
|
+
|
13
|
+
just include `require 'ixtlan/datamapper/optimistic'` and have model like:
|
14
|
+
|
15
|
+
class User
|
16
|
+
include DataMapper::Resource
|
17
|
+
|
18
|
+
property :id, Serial
|
19
|
+
property :name, String
|
20
|
+
|
21
|
+
timestamps :at
|
22
|
+
end
|
23
|
+
|
24
|
+
you need the timestamps to get it to work since the updated_at property will be used to determine if the object is stale or not.
|
25
|
+
|
26
|
+
now you get the object in an optimistic manner
|
27
|
+
|
28
|
+
User.optimistic_get!( updated_at, id )
|
29
|
+
User.optimistic_get( updated_at, id )
|
30
|
+
|
31
|
+
if will raise an Ixtlan::DataMapper::StaleObjectException in case the object with the given ```id``` exists but does carry a different updated_at timestamp. otherwise the ```optimistic_get``` and ```optimistic_get!``` behave the same as ```get``` and ```get!```.
|
32
|
+
|
33
|
+
now you get the object in an conditional manner
|
34
|
+
|
35
|
+
User.conditinal_get!( updated_at, id )
|
36
|
+
User.conditional_get( updated_at, id )
|
37
|
+
|
38
|
+
it the ```User``` when the updated_at does not match. when it matches it returns ```false```. in case the ```id``` does not exist, it will return either nil of DataMapper::ObjectNotFoundError. this allows constructs like
|
39
|
+
|
40
|
+
if u = User.conditinal_get!( request.last_modified, id )
|
41
|
+
response.last_modified = u.updated_at
|
42
|
+
response.write ....
|
43
|
+
else
|
44
|
+
# in case request.last_modified was nil
|
45
|
+
response.last_modified = u.updated_at
|
46
|
+
end
|
47
|
+
|
48
|
+
## Ixtlan::DataMapper::Immutable ##
|
49
|
+
|
50
|
+
class Group
|
51
|
+
include DataMapper::Resource
|
52
|
+
include Ixtlan::DataMapper::Immutable
|
53
|
+
|
54
|
+
property :id, Serial
|
55
|
+
property :name, String
|
56
|
+
end
|
57
|
+
|
58
|
+
you can create and delete those object but any attempt to change it the name ends in validation error.
|
59
|
+
|
60
|
+
## require 'ixtlan/datamapper/use_utc' ##
|
61
|
+
|
62
|
+
just convenient file to setup datamapper to use UTC timestamps
|
63
|
+
|
64
|
+
## Ixtlan::DataMapper:Collection ##
|
65
|
+
|
66
|
+
the collection is [virtus](http://github.com/solnic/virtus) object which helps to transport collections of DataMapper objects around. it has the ```total_count``` and an ```offset``` along an accessor for the list. the contructor deals with offset and limit on the datamapper query.
|
67
|
+
|
68
|
+
class UserCollection < Ixtlan::DataMapper::Collection
|
69
|
+
attribute :users, Array[User]
|
70
|
+
def data=( d )
|
71
|
+
self.users = d
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
this
|
76
|
+
|
77
|
+
UserCollection.new( User.all( :name.like => 'a%' ), 20, 10 )
|
78
|
+
|
79
|
+
will return 10 users starting with 'a' starting with 20th user from all possible users (with 'a').
|
80
|
+
|
81
|
+
|
82
|
+
Contributing
|
83
|
+
------------
|
84
|
+
|
85
|
+
1. Fork it
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create new Pull Request
|
90
|
+
|
91
|
+
meta-fu
|
92
|
+
-------
|
93
|
+
|
94
|
+
enjoy :)
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
|
3
|
+
task :default => [ :spec ]
|
4
|
+
|
5
|
+
task :spec do
|
6
|
+
Dir['spec/*_spec.rb'].each { |f| require File.expand_path(f).sub(/.rb$/, '') }
|
7
|
+
end
|
8
|
+
|
9
|
+
task :headers do
|
10
|
+
require 'rubygems'
|
11
|
+
require 'copyright_header'
|
12
|
+
|
13
|
+
s = Gem::Specification.load( Dir["*gemspec"].first )
|
14
|
+
|
15
|
+
args = {
|
16
|
+
:license => s.license,
|
17
|
+
:copyright_software => s.name,
|
18
|
+
:copyright_software_description => s.description,
|
19
|
+
:copyright_holders => s.authors,
|
20
|
+
:copyright_years => [Time.now.year],
|
21
|
+
:add_path => 'lib',
|
22
|
+
:output_dir => './'
|
23
|
+
}
|
24
|
+
|
25
|
+
command_line = CopyrightHeader::CommandLine.new( args )
|
26
|
+
command_line.execute
|
27
|
+
end
|
28
|
+
|
29
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'ixtlan-datamapper'
|
4
|
+
s.version = '0.1.0'
|
5
|
+
|
6
|
+
s.summary = 'collection of utilities for datamapper'
|
7
|
+
s.description = 'collection of utilities for datamapper: optimistic get, conditional get, use-utc timestampt, tag model as immutable, etc'
|
8
|
+
s.homepage = 'http://github.com/mkristian/ixtlan-datamapper'
|
9
|
+
|
10
|
+
s.authors = ['mkristian']
|
11
|
+
s.email = ['m.kristian@web.de']
|
12
|
+
|
13
|
+
s.files = Dir['MIT-LICENSE']
|
14
|
+
s.licenses << 'MIT'
|
15
|
+
# s.files += Dir['History.txt']
|
16
|
+
s.files += Dir['*.gemspec']
|
17
|
+
s.files += Dir['README.md']
|
18
|
+
s.files += Dir['lib/**/*']
|
19
|
+
s.files += Dir['spec/**/*']
|
20
|
+
s.files += Dir['*file']
|
21
|
+
s.test_files += Dir['spec/**/*_spec.rb']
|
22
|
+
s.add_runtime_dependency 'virtus', '~>1.0'
|
23
|
+
s.add_runtime_dependency 'dm-aggregates', '~>1.2'
|
24
|
+
s.add_development_dependency 'dm-timestamps', '~>1.2'
|
25
|
+
s.add_development_dependency 'dm-migrations', '~>1.2'
|
26
|
+
s.add_development_dependency 'dm-validations', '~>1.2'
|
27
|
+
s.add_development_dependency 'dm-sqlite-adapter', '~>1.2'
|
28
|
+
s.add_development_dependency 'rake', '~>10.3'
|
29
|
+
s.add_development_dependency 'minitest', '~>5.3'
|
30
|
+
end
|
31
|
+
|
32
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2012 mkristian
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
if defined?(Rails)
|
22
|
+
require 'ixtlan/optimistic/railtie'
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
module Ixtlan
|
3
|
+
module DataMapper
|
4
|
+
class Collection
|
5
|
+
include Virtus.model( #:mass_assignments => false,
|
6
|
+
:coerce => false )
|
7
|
+
|
8
|
+
attribute :offset, Integer
|
9
|
+
attribute :total_count, Integer
|
10
|
+
|
11
|
+
def initialize( data, offset = nil, count = nil )
|
12
|
+
super()
|
13
|
+
self.total_count = data.count
|
14
|
+
self.offset = offset.to_i
|
15
|
+
last = ( count ? count.to_i : self.total_count ) - 1 + self.offset
|
16
|
+
self.data = data[ self.offset..last ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def data=( d )
|
20
|
+
raise "not implemented"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{self.class}[ offset=#{offset} total_count=#{total_count} ]"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
module Ixtlan
|
3
|
+
module DataMapper
|
4
|
+
class Collection
|
5
|
+
# TODO no mass-assignments, etc
|
6
|
+
include Virtus.model( :coerce => false )
|
7
|
+
|
8
|
+
attribute :offset, Integer
|
9
|
+
attribute :total_count, Integer
|
10
|
+
|
11
|
+
def initialize( data, offset = nil, count = nil )
|
12
|
+
super()
|
13
|
+
self.total_count = data.count
|
14
|
+
self.offset = offset.to_i
|
15
|
+
last = ( count ? count.to_i : self.total_count ) - 1 + self.offset
|
16
|
+
self.data = data[ self.offset..last ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def data=( d )
|
20
|
+
raise "not implemented"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{self.class}[ offset=#{offset} total_count=#{total_count} ]"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'dm-aggregates'
|
2
|
+
module Ixtlan
|
3
|
+
module DataMapper
|
4
|
+
module ConditionalGet
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ConditionalGet
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
ONE_SECOND = 1.0/86400
|
13
|
+
|
14
|
+
def _conditional_get( method, modified_since, *id )
|
15
|
+
if modified_since.is_a? String
|
16
|
+
modified_since = DateTime.parse( modified_since.sub(/[.][0-9]+/, '') )
|
17
|
+
end
|
18
|
+
if modified_since.nil?
|
19
|
+
self.send( method, *id )
|
20
|
+
else
|
21
|
+
query = Hash[ key.collect { |k| k.name }.zip( id ) ]
|
22
|
+
query[ :updated_at.gte ] = modified_since
|
23
|
+
query[ :updated_at.lte ] = modified_since + ONE_SECOND
|
24
|
+
# return false when up-to-date
|
25
|
+
# that helps to distinguish from nil which means not-found
|
26
|
+
self.count( query ) == 0 ? self.send( method, *id ) : false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
public
|
31
|
+
|
32
|
+
def conditional_get!( modified_since, *id )
|
33
|
+
_conditional_get( :get!, modified_since, *id )
|
34
|
+
end
|
35
|
+
|
36
|
+
def conditional_get( modified_since, *id )
|
37
|
+
_conditional_get( :get, modified_since, *id )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
::DataMapper::Model.append_inclusions( ConditionalGet )
|
41
|
+
::DataMapper::Associations::OneToMany::Collection.send( :include,
|
42
|
+
ConditionalGet )
|
43
|
+
::DataMapper::Associations::ManyToMany::Collection.send( :include,
|
44
|
+
ConditionalGet )
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'dm-aggregates'
|
2
|
+
module Ixtlan
|
3
|
+
module DataMapper
|
4
|
+
module ConditionalGet
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ConditionalGet
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
ONE_SECOND = 1.0/86400
|
13
|
+
|
14
|
+
def _conditional_get( method, modified_since, *id )
|
15
|
+
if modified_since.is_a? String
|
16
|
+
modified_since = DateTime.parse( modified_since.sub(/[.][0-9]+/, '') )
|
17
|
+
end
|
18
|
+
if modified_since.nil?
|
19
|
+
self.send( method, *id )
|
20
|
+
else
|
21
|
+
modified_since = modified_since.new_offset( 0 )
|
22
|
+
query = Hash[ key.collect { |k| k.name }.zip( id ) ]
|
23
|
+
query[ :updated_at.gt ] = modified_since
|
24
|
+
query[ :updated_at.lt ] = modified_since + ONE_SECOND
|
25
|
+
# return false when up-to-date
|
26
|
+
# that helps to distinguish from nil which means not-found
|
27
|
+
self.count( query ) == 0 ? self.send( method, *id ) : false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
public
|
32
|
+
|
33
|
+
def conditional_get!( modified_since, *id )
|
34
|
+
_conditional_get( :get!, modified_since, *id )
|
35
|
+
end
|
36
|
+
|
37
|
+
def conditional_get( modified_since, *id )
|
38
|
+
_conditional_get( :get, modified_since, *id )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
::DataMapper::Model.append_inclusions( ConditionalGet )
|
42
|
+
::DataMapper::Associations::OneToMany::Collection.send( :include,
|
43
|
+
ConditionalGet )
|
44
|
+
::DataMapper::Associations::ManyToMany::Collection.send( :include,
|
45
|
+
ConditionalGet )
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module CubaPlugin
|
3
|
+
|
4
|
+
# matcher
|
5
|
+
def no_path
|
6
|
+
Proc.new { env[ 'PATH_INFO' ].empty? }
|
7
|
+
end
|
8
|
+
|
9
|
+
# matcher
|
10
|
+
def head
|
11
|
+
req.head?
|
12
|
+
end
|
13
|
+
|
14
|
+
def option
|
15
|
+
req.options?
|
16
|
+
end
|
17
|
+
|
18
|
+
# convenient method for status only responses
|
19
|
+
def no_body( status )
|
20
|
+
res.status = ::Rack::Utils.status_code( status )
|
21
|
+
res.write ::Rack::Utils::HTTP_STATUS_CODES[ res.status ]
|
22
|
+
res['Content-Type' ] = 'text/plain'
|
23
|
+
end
|
24
|
+
|
25
|
+
# params
|
26
|
+
def to_float( name, default = nil )
|
27
|
+
v = req[ name ]
|
28
|
+
if v
|
29
|
+
v.to_f
|
30
|
+
else
|
31
|
+
default
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_int( name, default = nil )
|
36
|
+
v = req[ name ]
|
37
|
+
if v
|
38
|
+
v.to_i
|
39
|
+
else
|
40
|
+
default
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_boolean( name, default = nil )
|
45
|
+
v = req[ name ]
|
46
|
+
if v
|
47
|
+
v == 'true'
|
48
|
+
else
|
49
|
+
default
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def offset_n_limit( method, set )
|
54
|
+
count = set.count
|
55
|
+
offset = to_int( 'offset' ).to_i
|
56
|
+
limit = ( to_int( 'count' ) || count ) - 1 + offset
|
57
|
+
{ method => set[ offset..limit ], :offset => offset, :total_count => count }
|
58
|
+
end
|
59
|
+
|
60
|
+
def last_modified( last )
|
61
|
+
if last
|
62
|
+
res[ 'Last-Modified' ] = rfc2616( last )
|
63
|
+
res[ 'Cache-Control' ] = "private, max-age=0, must-revalidate"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def modified_since
|
68
|
+
@modified_since ||=
|
69
|
+
if date = env[ 'HTTP_IF_MODIFIED_SINCE' ]
|
70
|
+
DateTime.parse( date )
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|