perry 0.4.0 → 0.5.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.
@@ -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