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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +94 -0
  5. data/Rakefile +29 -0
  6. data/ixtlan-datamapper.gemspec +32 -0
  7. data/lib/ixtlan-datamapper.rb +23 -0
  8. data/lib/ixtlan/datamapper/collection.rb +28 -0
  9. data/lib/ixtlan/datamapper/collection.rb~ +28 -0
  10. data/lib/ixtlan/datamapper/conditional_get.rb +46 -0
  11. data/lib/ixtlan/datamapper/conditional_get.rb~ +47 -0
  12. data/lib/ixtlan/datamapper/cuba_plugin.rb~ +74 -0
  13. data/lib/ixtlan/datamapper/immutable.rb +18 -0
  14. data/lib/ixtlan/datamapper/immutable.rb~ +83 -0
  15. data/lib/ixtlan/datamapper/modified.rb~ +83 -0
  16. data/lib/ixtlan/datamapper/modified_by.rb +39 -0
  17. data/lib/ixtlan/datamapper/modified_by.rb~ +39 -0
  18. data/lib/ixtlan/datamapper/optimistic.rb~ +53 -0
  19. data/lib/ixtlan/datamapper/optimistic_get.rb +71 -0
  20. data/lib/ixtlan/datamapper/optimistic_get.rb~ +72 -0
  21. data/lib/ixtlan/datamapper/stale_check.rb~ +49 -0
  22. data/lib/ixtlan/datamapper/stale_object_exception.rb +26 -0
  23. data/lib/ixtlan/datamapper/stale_object_exception.rb~ +26 -0
  24. data/lib/ixtlan/datamapper/use_utc.rb +4 -0
  25. data/lib/ixtlan/datamapper/use_utc.rb~ +5 -0
  26. data/lib/ixtlan/datamapper/validations_ext.rb +61 -0
  27. data/spec/collection_spec.rb +57 -0
  28. data/spec/collection_spec.rb~ +54 -0
  29. data/spec/conditional_get_spec.rb +69 -0
  30. data/spec/conditional_get_spec.rb~ +70 -0
  31. data/spec/datamapper_spec.rb~ +74 -0
  32. data/spec/immutable_spec.rb +33 -0
  33. data/spec/immutable_spec.rb~ +70 -0
  34. data/spec/modified_by_spec.rb +54 -0
  35. data/spec/modified_by_spec.rb~ +33 -0
  36. data/spec/optimistic_get_spec.rb +57 -0
  37. data/spec/spec_helper.rb +15 -0
  38. data/spec/spec_helper.rb~ +12 -0
  39. 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
@@ -0,0 +1,9 @@
1
+ # -*- mode: ruby -*-
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'copyright-header', '~>1.0', :platform => :mri, :group => :copyright
8
+
9
+ # vim: syntax=Ruby
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
+ * [![Build Status](https://secure.travis-ci.org/mkristian/ixtlan-datamapper.png)](http://travis-ci.org/mkristian/ixtlan-datamapper)
4
+ * [![Dependency Status](https://gemnasium.com/mkristian/ixtlan-datamapper.png)](https://gemnasium.com/mkristian/ixtlan-datamapper)
5
+ * [![Code Climate](https://codeclimate.com/github/mkristian/ixtlan-datamapper.png)](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