perry 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ class Perry::Middlewares::CacheRecords; end
2
+ require 'perry/middlewares/cache_records/store'
3
+ require 'perry/middlewares/cache_records/entry'
4
+ require 'perry/middlewares/cache_records/scopes'
5
+ require 'digest/md5'
6
+
7
+ class Perry::Middlewares::CacheRecords
8
+ include Perry::Logger
9
+
10
+ attr_accessor :record_count_threshold
11
+
12
+ # TRP: Default to a 5 minute cache
13
+ DEFAULT_LONGEVITY = 5*60
14
+
15
+ def reset_cache_store(default_longevity=DEFAULT_LONGEVITY)
16
+ @cache_store = Perry::Middlewares::CacheRecords::Store.new(default_longevity)
17
+ end
18
+
19
+ def cache_store
20
+ @cache_store || reset_cache_store
21
+ end
22
+
23
+ def initialize(adapter, config={})
24
+ @adapter = adapter
25
+ self.record_count_threshold = config[:record_count_threshold]
26
+ end
27
+
28
+ def call(options)
29
+ if options[:relation]
30
+ call_with_cache(options)
31
+ else
32
+ @adapter.call(options)
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def call_with_cache(options)
39
+ relation = options[:relation]
40
+ modifiers = relation.modifiers_value
41
+ query = relation.to_hash
42
+
43
+ reset_cache_store if modifiers[:reset_cache]
44
+ get_fresh = modifiers[:fresh]
45
+
46
+ key = key_for_query(query)
47
+ cached_values = self.cache_store.read(key)
48
+
49
+
50
+ if cached_values && !get_fresh
51
+ log(query, "CACHE #{relation.klass.name}")
52
+ cached_values
53
+ else
54
+ fresh_values = @adapter.call(options)
55
+ self.cache_store.write(key, fresh_values) if should_store_in_cache?(fresh_values)
56
+ fresh_values
57
+ end
58
+ end
59
+
60
+ def key_for_query(query_hash)
61
+ Digest::MD5.hexdigest(self.class.to_s + query_hash.to_a.sort { |a,b| a.to_s.first <=> b.to_s.first }.inspect)
62
+ end
63
+
64
+ def should_store_in_cache?(fresh_values)
65
+ !self.record_count_threshold || fresh_values.size <= self.record_count_threshold
66
+ end
67
+ end
@@ -0,0 +1,66 @@
1
+ class Perry::Middlewares::ModelBridge
2
+
3
+ def initialize(adapter, config={})
4
+ @adapter = adapter
5
+ @config = config
6
+ end
7
+
8
+ def call(options)
9
+ result = @adapter.call(options)
10
+
11
+ case options[:mode]
12
+ when :read
13
+ build_models_from_records(result, options)
14
+ when :write
15
+ update_model_after_save(result, options[:object])
16
+ result
17
+ when :delete
18
+ update_model_after_delete(result, options[:object])
19
+ result
20
+ else
21
+ result
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def build_models_from_records(records, options)
28
+ if options[:relation]
29
+ records.collect do |attributes|
30
+ options[:relation].klass.new_from_data_store(attributes)
31
+ end
32
+ else
33
+ records
34
+ end
35
+ end
36
+
37
+ def update_model_after_save(response, model)
38
+ model.saved = response.success
39
+ if model.saved
40
+ if model.new_record?
41
+ key = response.model_attributes[model.primary_key]
42
+ raise Perry::PerryError.new('primary key not included in response') if key.nil?
43
+ model.send("#{model.primary_key}=", key)
44
+ end
45
+ model.new_record = false
46
+ model.reload unless model.read_adapter.nil?
47
+ else
48
+ add_errors_to_model(response, model, 'not saved')
49
+ end
50
+ end
51
+
52
+ def update_model_after_delete(response, model)
53
+ if response.success
54
+ model.freeze!
55
+ else
56
+ add_errors_to_model(response, model, 'not deleted')
57
+ end
58
+ end
59
+
60
+ def add_errors_to_model(response, model, default_message)
61
+ errors = response.errors
62
+ errors[:base] = default_message if errors.empty?
63
+ model.errors.merge!(errors)
64
+ end
65
+
66
+ end
@@ -0,0 +1,8 @@
1
+ module Perry::Middlewares
2
+
3
+ autoload :ModelBridge, 'perry/middlewares/model_bridge'
4
+ autoload :PreloadAssociations, 'perry/middlewares/preload_associations'
5
+ autoload :CacheRecords, 'perry/middlewares/cache_records'
6
+
7
+ end
8
+
@@ -0,0 +1,75 @@
1
+ module Perry::Persistence
2
+ class Response
3
+
4
+ @@parsers = { :json => ::JSON }
5
+ ATTRIBUTES = [:success, :status, :meta, :raw, :raw_format, :parsed]
6
+
7
+ # A boolean value reflecting whether or not the response was successful
8
+ attr_accessor :success
9
+
10
+ # A more detailed report of the success/failure of the response
11
+ attr_accessor :status
12
+
13
+ # Any adapter specific response metadata
14
+ attr_accessor :meta
15
+
16
+ # Raw response -- format specified by raw_format
17
+ attr_accessor :raw
18
+
19
+ # Format of the raw response
20
+ attr_accessor :raw_format
21
+
22
+ # Parsed raw data
23
+ attr_writer :parsed
24
+
25
+ # @attrs: Sets any values listed in ATTRIBUTES
26
+ def initialize(attrs={})
27
+ ATTRIBUTES.each do |attr|
28
+ self.send("#{attr}=", attrs[attr])
29
+ end
30
+ end
31
+
32
+ def parsed
33
+ if parser = self.class.parsers[self.raw_format]
34
+ begin
35
+ @parsed ||= parser.parse(self.raw)
36
+ rescue Exception => err
37
+ Perry.logger.error("Failure parsing raw response #{err.inspect}")
38
+ Perry.logger.error("Response: #{self.inspect}")
39
+ nil
40
+ end
41
+ else
42
+ @parsed
43
+ end
44
+ end
45
+
46
+ def self.parsers
47
+ @@parsers
48
+ end
49
+
50
+ def model_attributes
51
+ # return the inner hash if nested
52
+ if parsed_hash.keys.size == 1 && parsed_hash[parsed_hash.keys.first].is_a?(Hash)
53
+ parsed_hash[parsed_hash.keys.first]
54
+ else
55
+ parsed_hash
56
+ end.symbolize_keys
57
+ end
58
+
59
+ def errors
60
+ parsed_hash.symbolize_keys
61
+ end
62
+
63
+ protected
64
+
65
+ def parsed_hash
66
+ if parsed.is_a?(Hash)
67
+ parsed
68
+ else
69
+ {}
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+
@@ -1,3 +1,5 @@
1
+ require 'perry/persistence/response'
2
+
1
3
  module Perry::Persistence
2
4
 
3
5
  module ClassMethods
@@ -24,7 +26,8 @@ module Perry::Persistence
24
26
  end
25
27
 
26
28
  def save
27
- write_adapter.write(self)
29
+ raise Perry::PerryError.new("cannot write a frozen object") if frozen?
30
+ write_adapter.call(:write, :object => self).success
28
31
  end
29
32
 
30
33
  def save!
@@ -41,10 +44,44 @@ module Perry::Persistence
41
44
  end
42
45
 
43
46
  def destroy
44
- write_adapter.delete(self) unless self.new_record?
47
+ raise Perry::PerryError.new("cannot destroy a frozen object") if frozen?
48
+ unless self.new_record? || self.send(primary_key).nil?
49
+ write_adapter.call(:delete, :object => self).success
50
+ end
45
51
  end
46
52
  alias :delete :destroy
47
53
 
54
+ def destroy!
55
+ destroy or raise Perry::RecordNotSaved
56
+ end
57
+ alias :delete! :destroy!
58
+
59
+ def reload
60
+ self.attributes = self.class.where(primary_key => self.send(primary_key)).first.attributes
61
+ end
62
+
63
+ # Calls Object#freeze on the model and on the attributes hash in addition to
64
+ # calling #freeze! to prevent the model from being saved or destroyed.
65
+ #
66
+ def freeze
67
+ freeze!
68
+ attributes.freeze
69
+ super
70
+ end
71
+
72
+ # Prevents the model from being saved or destroyed in the future while still
73
+ # allowing the model and its attributes hash to be modified.
74
+ #
75
+ def freeze!
76
+ @frozen = true
77
+ end
78
+
79
+ # Returns true if ether #freeze or #freeze! has been called on the model.
80
+ #
81
+ def frozen?
82
+ !!@frozen
83
+ end
84
+
48
85
  end
49
86
 
50
87
  def self.included(receiver)
@@ -53,3 +90,4 @@ module Perry::Persistence
53
90
  end
54
91
 
55
92
  end
93
+
@@ -0,0 +1,61 @@
1
+ ##
2
+ # = Perry::Processors::PreloadAssociations
3
+ #
4
+ # This adapter processor will allow associations to be eager loaded after the original records are fetched.
5
+ # Any associations specified in an :includes option will be loaded for all records in the query
6
+ # (each association in a single request).
7
+ #
8
+ # == Configuration Options:
9
+ #
10
+ # None
11
+ #
12
+ # == Modifiers:
13
+ #
14
+ # None
15
+ #
16
+ #
17
+ #
18
+ class Perry::Processors::PreloadAssociations
19
+
20
+ def initialize(adapter, config={})
21
+ @adapter = adapter
22
+ @config = config
23
+ end
24
+
25
+ def call(options)
26
+ results = @adapter.call(options)
27
+
28
+ unless results.empty?
29
+ relation = options[:relation]
30
+ (includes = relation.to_hash[:includes] || {}).keys.each do |association_id|
31
+ association = relation.klass.defined_associations[association_id.to_sym]
32
+ raise Perry::AssociationNotFound, "unknown association #{association_id}" unless association
33
+ eager_records = association.scope(results).includes(includes[association_id]).all(:modifiers => options[:relation].modifiers_value)
34
+
35
+ results.each do |result|
36
+ scope = association.scope(result)
37
+ case association.type
38
+ when :has_one, :has_many
39
+ scope.records = eager_records.select do |record|
40
+ record.send(association.foreign_key) == result.send(association.primary_key)
41
+ end
42
+ if association.collection?
43
+ result.send("#{association.id}=", scope)
44
+ else
45
+ result.send("#{association.id}=", scope.records.first)
46
+ end
47
+ when :belongs_to
48
+ scope.records = eager_records.select do |record|
49
+ record.send(association.primary_key) == result.send(association.foreign_key)
50
+ end
51
+ result.send("#{association.id}=", scope.records.first)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ results
58
+ end
59
+
60
+ end
61
+
@@ -0,0 +1,5 @@
1
+ module Perry::Processors
2
+
3
+ autoload :PreloadAssociations, 'perry/processors/preload_associations'
4
+
5
+ end
@@ -3,10 +3,12 @@ module Perry::FinderMethods
3
3
  def find(ids_or_mode, options={})
4
4
  case ids_or_mode
5
5
  when Fixnum, String
6
- self.where(:id => ids_or_mode.to_i).first(options) || raise(Perry::RecordNotFound, "Could not find #{@klass} with :id = #{ids_or_mode}")
6
+ self.where(primary_key => ids_or_mode).first(options) || raise(Perry::RecordNotFound, "Could not find #{@klass} with :#{primary_key} = #{ids_or_mode}")
7
7
  when Array
8
- self.where(:id => ids_or_mode).all(options).tap do |result|
9
- raise Perry::RecordNotFound, "Couldn't find all #{@klass} with ids (#{ids_or_mode.join(',')}) (expected #{ids_or_mode.size} records but got #{result.size})." unless result.size == ids_or_mode.size
8
+ self.where(primary_key => ids_or_mode).all(options).tap do |result|
9
+ unless result.size == ids_or_mode.size
10
+ raise Perry::RecordNotFound, "Couldn't find all #{@klass} with ids (#{ids_or_mode.join(',')}) (expected #{ids_or_mode.size} records but got #{result.size})."
11
+ end
10
12
  end
11
13
  when :all
12
14
  self.all(options)
@@ -37,7 +39,7 @@ module Perry::FinderMethods
37
39
  relation = clone
38
40
  return relation unless options
39
41
 
40
- [:joins, :limit, :offset, :order, :select, :group, :having, :from, :fresh, :includes].each do |finder|
42
+ Perry::Relation::QUERY_METHODS.each do |finder|
41
43
  relation = relation.send(finder, options[finder]) if options[finder]
42
44
  end
43
45
 
@@ -50,6 +52,8 @@ module Perry::FinderMethods
50
52
 
51
53
  relation = relation.sql(options[:sql]) if options.has_key?(:sql)
52
54
 
55
+ relation = relation.modifiers(options[:modifiers]) if options.has_key?(:modifiers)
56
+
53
57
  relation
54
58
  end
55
59
 
@@ -0,0 +1,37 @@
1
+ module Perry::Modifiers
2
+ attr_accessor :modifiers_value, :modifiers_array
3
+
4
+ # The modifiers query method allows you to 'extend' your query by adding parameters that
5
+ # middlewares or adapters can use to modify the query itself or modify how the query is executed.
6
+ # This method expects a hash or a Proc that returns a hash as its only argument and will merge
7
+ # that hash onto a master hash of query modifiers.
8
+ #
9
+ # For most purposes, the modifiers method acts like any other query method. For example, you
10
+ # can chain it with other query methods on a relation, and you can build scopes with it.
11
+ # One exception is that modifiers are not included in the hash returned by Relation#to_hash.
12
+ # This exception is intended to discourage adapters from passing modifiers to backends external
13
+ # to perry (such as a data server or a webservice call) whose behavior is undefined with respect
14
+ # to these additional parameters/
15
+ #
16
+ # See also 'perry/middlewares/cache_records/scopes.rb' for examples of how to use the modifiers
17
+ # pseudo-query method.
18
+ def modifiers(value={})
19
+ clone.tap do |r|
20
+ if value.nil?
21
+ r.modifiers_array = [] # wipeout all previously set modifiers
22
+ else
23
+ r.modifiers_array ||= []
24
+ r.modifiers_array.push(value)
25
+ end
26
+ end
27
+ end
28
+
29
+ def modifiers_value
30
+ self.modifiers_array ||= []
31
+ {}.tap do |hash|
32
+ self.modifiers_array.each do |value|
33
+ hash.merge!(value.is_a?(Proc) ? value.call : value)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,8 +1,7 @@
1
1
  module Perry::QueryMethods
2
2
  # TRP: Define each of the variables the query options will be stored in.
3
- attr_accessor :select_values, :group_values, :order_values, :joins_values, :includes_values, :where_values, :having_values,
4
- :limit_value, :offset_value, :from_value,
5
- :raw_sql_value, :fresh_value
3
+ attr_accessor :select_values, :group_values, :order_values, :joins_values, :includes_value, :where_values, :having_values,
4
+ :limit_value, :offset_value, :from_value, :raw_sql_value
6
5
 
7
6
  def select(*args)
8
7
  if block_given?
@@ -26,7 +25,7 @@ module Perry::QueryMethods
26
25
 
27
26
  def includes(*args)
28
27
  args.reject! { |a| a.nil? }
29
- clone.tap { |r| r.includes_values += (r.includes_values + args).flatten.uniq if args_valid? args }
28
+ clone.tap { |r| r.includes_value = (r.includes_value || {}).deep_merge(sanitize_includes(args)) if args_valid? args }
30
29
  end
31
30
 
32
31
  def where(*args)
@@ -49,13 +48,6 @@ module Perry::QueryMethods
49
48
  clone.tap { |r| r.from_value = table }
50
49
  end
51
50
 
52
- def fresh(val=true)
53
- clone.tap do |r|
54
- r.fresh_value = val
55
- r.reset_queries if r.fresh_value
56
- end
57
- end
58
-
59
51
  def sql(raw_sql)
60
52
  clone.tap { |r| r.raw_sql_value = raw_sql }
61
53
  end
@@ -66,4 +58,20 @@ module Perry::QueryMethods
66
58
  args.respond_to?(:empty?) ? !args.empty? : !!args
67
59
  end
68
60
 
61
+ # This will allow for the nested structure
62
+ def sanitize_includes(values)
63
+ case values
64
+ when Hash
65
+ values.keys.inject({}) do |hash, key|
66
+ hash.merge key => sanitize_includes(values[key])
67
+ end
68
+ when Array
69
+ values.inject({}) { |hash, val| hash.merge sanitize_includes(val) }
70
+ when String, Symbol
71
+ { values.to_sym => {} }
72
+ else
73
+ {}
74
+ end
75
+ end
76
+
69
77
  end
@@ -1,5 +1,6 @@
1
1
  require 'perry/relation/query_methods'
2
2
  require 'perry/relation/finder_methods'
3
+ require 'perry/relation/modifiers'
3
4
 
4
5
  # TRP: The concepts behind this class are heavily influenced by ActiveRecord::Relation v.3.0.0RC1
5
6
  # => http://github.com/rails/rails
@@ -8,13 +9,15 @@ class Perry::Relation
8
9
  attr_reader :klass
9
10
  attr_accessor :records
10
11
 
11
- SINGLE_VALUE_METHODS = [:limit, :offset, :from, :fresh]
12
- MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :includes, :where, :having]
12
+ SINGLE_VALUE_METHODS = [:limit, :offset, :includes, :from]
13
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
13
14
 
14
- FINDER_OPTIONS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS + [:conditions, :search, :sql]
15
+ QUERY_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
16
+ FINDER_OPTIONS = QUERY_METHODS + [:conditions, :search, :sql]
15
17
 
16
18
  include Perry::QueryMethods
17
19
  include Perry::FinderMethods
20
+ include Perry::Modifiers
18
21
 
19
22
  def initialize(klass)
20
23
  @klass = klass
@@ -36,6 +39,8 @@ class Perry::Relation
36
39
  merged_relation = merged_relation.send(option, *r.send("#{option}_values"))
37
40
  end
38
41
 
42
+ merged_relation.send(:modifiers_array=, r.send(:modifiers_array))
43
+
39
44
  merged_relation
40
45
  end
41
46
 
@@ -76,7 +81,7 @@ class Perry::Relation
76
81
  end
77
82
 
78
83
  def eager_load?
79
- @includes_values && !@includes_values.empty?
84
+ @includes_value
80
85
  end
81
86
 
82
87
  def inspect
data/lib/perry/version.rb CHANGED
@@ -2,7 +2,7 @@ module Perry
2
2
  module Version
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 4
5
+ MINOR = 5
6
6
  TINY = 0
7
7
 
8
8
  def self.to_s # :nodoc:
data/lib/perry.rb CHANGED
@@ -2,9 +2,11 @@
2
2
  require 'active_support/core_ext/array'
3
3
  require 'active_support/core_ext/class/inheritable_attributes'
4
4
  require 'active_support/core_ext/hash/deep_merge'
5
+ require 'active_support/core_ext/hash/keys'
5
6
  begin
6
7
  require 'active_support/core_ext/duplicable' #ActiveSupport 2.3.x
7
8
  Hash.send(:include, ActiveSupport::CoreExtensions::Hash::DeepMerge) unless Hash.instance_methods.include?('deep_merge')
9
+ Hash.send(:include, ActiveSupport::CoreExtensions::Hash::Keys) unless Hash.instance_methods.include?('symbolize_keys')
8
10
  rescue LoadError => exception
9
11
  require 'active_support/core_ext/object/duplicable' #ActiveSupport 3.0.x
10
12
  end
@@ -13,6 +15,9 @@ require 'active_support/core_ext/module/delegation'
13
15
  # TRP: Used for pretty logging
14
16
  autoload :Benchmark, 'benchmark'
15
17
 
18
+ # If needed to parse raw adapter responses
19
+ require 'json'
20
+
16
21
  require 'ostruct'
17
22
 
18
23
  # TRP: Perry core_ext
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perry
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
8
+ - 5
9
9
  - 0
10
- version: 0.4.0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Travis Petticrew
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-04-14 00:00:00 -05:00
18
+ date: 2011-05-13 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -112,6 +112,22 @@ dependencies:
112
112
  version: 1.3.0
113
113
  type: :runtime
114
114
  version_requirements: *id006
115
+ - !ruby/object:Gem::Dependency
116
+ name: json
117
+ prerelease: false
118
+ requirement: &id007 !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 11
124
+ segments:
125
+ - 1
126
+ - 4
127
+ - 6
128
+ version: 1.4.6
129
+ type: :runtime
130
+ version_requirements: *id007
115
131
  description:
116
132
  email: bobo@petticrew.net
117
133
  executables: []
@@ -128,19 +144,25 @@ files:
128
144
  - lib/perry/adapters/restful_http_adapter.rb
129
145
  - lib/perry/adapters.rb
130
146
  - lib/perry/association.rb
131
- - lib/perry/association_preload.rb
132
147
  - lib/perry/associations/common.rb
133
148
  - lib/perry/associations/contains.rb
134
149
  - lib/perry/associations/external.rb
135
150
  - lib/perry/base.rb
136
- - lib/perry/cacheable/entry.rb
137
- - lib/perry/cacheable/store.rb
138
- - lib/perry/cacheable.rb
139
151
  - lib/perry/core_ext/kernel/singleton_class.rb
140
152
  - lib/perry/errors.rb
141
153
  - lib/perry/logger.rb
154
+ - lib/perry/middlewares/cache_records/entry.rb
155
+ - lib/perry/middlewares/cache_records/scopes.rb
156
+ - lib/perry/middlewares/cache_records/store.rb
157
+ - lib/perry/middlewares/cache_records.rb
158
+ - lib/perry/middlewares/model_bridge.rb
159
+ - lib/perry/middlewares.rb
160
+ - lib/perry/persistence/response.rb
142
161
  - lib/perry/persistence.rb
162
+ - lib/perry/processors/preload_associations.rb
163
+ - lib/perry/processors.rb
143
164
  - lib/perry/relation/finder_methods.rb
165
+ - lib/perry/relation/modifiers.rb
144
166
  - lib/perry/relation/query_methods.rb
145
167
  - lib/perry/relation.rb
146
168
  - lib/perry/scopes/conditions.rb
@@ -179,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
201
  requirements: []
180
202
 
181
203
  rubyforge_project:
182
- rubygems_version: 1.4.2
204
+ rubygems_version: 1.6.2
183
205
  signing_key:
184
206
  specification_version: 3
185
207
  summary: Ruby library for querying and mapping data through generic interfaces