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.
data/Rakefile CHANGED
@@ -24,6 +24,7 @@ spec = Gem::Specification.new do |s|
24
24
 
25
25
  s.add_dependency("activesupport", [">= 2.3.0"])
26
26
  s.add_dependency("bertrpc", [">= 1.3.0"])
27
+ s.add_dependency("json", [">=1.4.6"])
27
28
  end
28
29
 
29
30
  Rake::GemPackageTask.new(spec) do |pkg|
@@ -52,7 +53,7 @@ begin
52
53
  t.libs = ['test']
53
54
  t.test_files = FileList["test/**/*_test.rb"]
54
55
  t.verbose = true
55
- t.rcov_opts = ['--text-report', "-x #{Gem.path}", '-x /Library/Ruby', '-x /usr/lib/ruby']
56
+ t.rcov_opts = ['--text-report', "-x #{Gem.path.join(',')}", '-x /Library/Ruby', '-x /usr/lib/ruby']
56
57
  end
57
58
 
58
59
  task :default => :coverage
@@ -1,92 +1,217 @@
1
1
  require 'perry/logger'
2
2
 
3
- module Perry::Adapters
4
- class AbstractAdapter
5
- include Perry::Logger
3
+ ##
4
+ # = Perry::Adapters::AbstractAdapter
5
+ #
6
+ # This is the base class from which all adapters should inherit from. Subclasses should overwrite
7
+ # one or all of read, write, and/or delete. They should also register themselves with a
8
+ # unique name using the register_as class method.
9
+ #
10
+ # Adapters contain a stack of code that is executed on each request. Here is a diagram of the basic
11
+ # anatomy of the adaper stack:
12
+ #
13
+ # +----------------+
14
+ # | Perry::Base |
15
+ # +----------------+
16
+ # |
17
+ # +----------------+
18
+ # | Processors |
19
+ # +----------------+
20
+ # |
21
+ # +----------------+
22
+ # | ModelBridge |
23
+ # +----------------+
24
+ # |
25
+ # +----------------+
26
+ # | Middlewares |
27
+ # +----------------+
28
+ # |
29
+ # +----------------+
30
+ # | Adapter |
31
+ # +----------------+
32
+ #
33
+ # Each request is routed through registred processors, the ModelBridge, and registered middlewares
34
+ # before reaching the adapter. After the adapter does its operation the return value passes through
35
+ # each item in the stack allowing stack items to do both custom pre and post processing to every
36
+ # request.
37
+ #
38
+ # == Configuration
39
+ #
40
+ # You can configure your adapters using the configure method on Perry::Base
41
+ #
42
+ # configure(:read) do |config|
43
+ # config.adapter_var_1 = :custom_value
44
+ # config.adapter_var_2 = [:some, :values]
45
+ # end
46
+ #
47
+ # This block creates a new configuration context. Each context is merged onto the previous context
48
+ # allowing subclasses to override configuration set by their parent class.
49
+ #
50
+ # == Middlewares
51
+ #
52
+ # Middlewares allow you to add custom logic between the model and the adapter. A good example is
53
+ # caching. A caching middleware could be implemented that intercepted a request to the adapter and
54
+ # returned the cached value for that request. If the request is a cache miss it could pass the
55
+ # request on to the adapter, and then cache the result for subsequent calls of the same request.
56
+ #
57
+ # This is an example mof a no-op middleware:
58
+ #
59
+ # class NoOpMiddleware
60
+ #
61
+ # def initialize(adapter, config={})
62
+ # @adapter = adapter
63
+ # @config = config
64
+ # end
65
+ #
66
+ # def call(options)
67
+ # @adapter.call(options)
68
+ # end
69
+ #
70
+ # end
71
+ #
72
+ # Though this doesn't do anything it serves to demonstrate the basic structure of a middleware.
73
+ # Logic could be added to perform caching, custom querying, or custom result processing.
74
+ #
75
+ # Middlewares can also be chained to perform several independent actions. Middlewares are configured
76
+ # through a custom configuration method:
77
+ #
78
+ # configure(:read) do |config|
79
+ # config.add_middleware(MyMiddleware, :config => 'var', :foo => 'bar')
80
+ # end
81
+ #
82
+ # == ModelBridge
83
+ #
84
+ # The ModelBridge is simply a middleware that is always installed. It instantiates the records from
85
+ # the data returned by the adapter. It "bridges" the raw data to the mapped object.
86
+ #
87
+ # == Processors
88
+ #
89
+ # Much like middlewares, processors allow you to insert logic into the request stack. The
90
+ # differentiation is that processors are able to manipulate the instantiated objects rather than
91
+ # just the raw data. Processors have access to the objects immediately before passing the data back
92
+ # to the model space.
93
+ #
94
+ # The interface for a processor is identical to that of a middleware. The return value of the call
95
+ # to adapter; however, is an array of Perry::Base objects rather than Hashes of attributes.
96
+ #
97
+ # Configuration is also very similar to middlewares:
98
+ #
99
+ # configure(:read) do |config|
100
+ # config.add_processor(MyProcessor, :config => 'var', :foo => 'bar')
101
+ # end
102
+ #
103
+ #
104
+ class Perry::Adapters::AbstractAdapter
105
+ include Perry::Logger
106
+
107
+ attr_accessor :config
108
+ attr_reader :type
109
+ @@registered_adapters ||= {}
110
+
111
+ # Accepts type as :read, :write, or :delete and a base configuration context for this adapter.
112
+ def initialize(type, config)
113
+ @type = type.to_sym
114
+ @configuration_contexts = config.is_a?(Array) ? config : [config]
115
+ end
6
116
 
7
- attr_accessor :config
8
- attr_reader :type
9
- @@registered_adapters ||= {}
117
+ # Wrapper to the standard init method that will lookup the adapter's class based on its registered
118
+ # symbol name.
119
+ def self.create(type, config)
120
+ klass = @@registered_adapters[type.to_sym]
121
+ klass.new(type, config)
122
+ end
10
123
 
11
- def initialize(type, config)
12
- @type = type.to_sym
13
- @configuration_contexts = config.is_a?(Array) ? config : [config]
14
- end
124
+ # Return a new adapter of the same type that adds the given configuration context
125
+ def extend_adapter(config)
126
+ config = config.is_a?(Array) ? config : [config]
127
+ self.class.create(self.type, @configuration_contexts + config)
128
+ end
15
129
 
16
- def self.create(type, config)
17
- klass = @@registered_adapters[type.to_sym]
18
- klass.new(type, config)
19
- end
130
+ # return the merged configuration object
131
+ def config
132
+ @config ||= build_configuration
133
+ end
20
134
 
21
- def extend_adapter(config)
22
- config = config.is_a?(Array) ? config : [config]
23
- self.class.create(self.type, @configuration_contexts + config)
135
+ # runs the adapter in the specified type mode -- designed to work with the middleware stack
136
+ def call(mode, options)
137
+ @stack ||= self.stack_items.inject(self.method(mode)) do |below, (above_klass, above_config)|
138
+ above_klass.new(below, above_config)
24
139
  end
25
140
 
26
- def config
27
- @config ||= build_configuration
28
- end
141
+ options[:mode] = mode.to_sym
142
+ @stack.call(options)
143
+ end
29
144
 
30
- def call(mode, options)
31
- @stack ||= self.middlewares.reverse.inject(self.method(mode)) do |below, (above_klass, above_config)|
32
- above_klass.new(below, above_config)
33
- end
145
+ # Return an array of added middlewares
146
+ def middlewares
147
+ self.config[:middlewares] || []
148
+ end
34
149
 
35
- @stack.call(options)
36
- end
150
+ # Return an array of added processors
151
+ def processors
152
+ self.config[:processors] || []
153
+ end
37
154
 
38
- def middlewares
39
- self.config[:middlewares] || []
40
- end
155
+ # Abstract read method -- overridden by subclasses
156
+ def read(options)
157
+ raise(NotImplementedError,
158
+ "You must not use the abstract adapter. Implement an adapter that extends the " +
159
+ "Perry::Adapters::AbstractAdapter class and overrides this method.")
160
+ end
41
161
 
42
- def read(options)
43
- raise(NotImplementedError,
44
- "You must not use the abstract adapter. Implement an adapter that extends the " +
45
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
46
- end
162
+ # Abstract write method -- overridden by subclasses
163
+ def write(object)
164
+ raise(NotImplementedError,
165
+ "You must not use the abstract adapter. Implement an adapter that extends the " +
166
+ "Perry::Adapters::AbstractAdapter class and overrides this method.")
167
+ end
47
168
 
48
- def write(object)
49
- raise(NotImplementedError,
50
- "You must not use the abstract adapter. Implement an adapter that extends the " +
51
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
52
- end
169
+ # Abstract delete method -- overridden by subclasses
170
+ def delete(object)
171
+ raise(NotImplementedError,
172
+ "You must not use the abstract adapter. Implement an adapter that extends the " +
173
+ "Perry::Adapters::AbstractAdapter class and overrides this method.")
174
+ end
53
175
 
54
- def delete(object)
55
- raise(NotImplementedError,
56
- "You must not use the abstract adapter. Implement an adapter that extends the " +
57
- "Perry::Adapters::AbstractAdapter class and overrides this method.")
58
- end
176
+ # New adapters should register themselves using this method
177
+ def self.register_as(name)
178
+ @@registered_adapters[name.to_sym] = self
179
+ end
59
180
 
60
- def self.register_as(name)
61
- @@registered_adapters[name.to_sym] = self
62
- end
181
+ protected
63
182
 
64
- private
183
+ def stack_items
184
+ (processors + [Perry::Middlewares::ModelBridge] + middlewares).reverse
185
+ end
65
186
 
66
- # TRP: Run each configure block in order of class hierarchy / definition and merge the results.
67
- def build_configuration
68
- @configuration_contexts.inject({}) do |sum, config|
69
- if config.is_a?(Hash)
70
- sum.merge(config)
71
- else
72
- AdapterConfig.new(sum).tap { |ac| config.call(ac) }.marshal_dump
73
- end
187
+ # TRP: Run each configure block in order of class hierarchy / definition and merge the results.
188
+ def build_configuration
189
+ @configuration_contexts.inject({}) do |sum, config|
190
+ if config.is_a?(Hash)
191
+ sum.merge(config)
192
+ else
193
+ AdapterConfig.new(sum).tap { |ac| config.call(ac) }.marshal_dump
74
194
  end
75
195
  end
196
+ end
76
197
 
77
- class AdapterConfig < OpenStruct
198
+ class AdapterConfig < OpenStruct
78
199
 
79
- def add_middleware(klass, config={})
80
- self.middlewares ||= []
81
- self.middlewares << [klass, config]
82
- end
200
+ def add_middleware(klass, config={})
201
+ self.middlewares ||= []
202
+ self.middlewares << [klass, config]
203
+ end
83
204
 
84
- def to_hash
85
- marshal_dump
86
- end
205
+ def add_processor(klass, config={})
206
+ self.processors ||= []
207
+ self.processors << [klass, config]
208
+ end
87
209
 
210
+ def to_hash
211
+ marshal_dump
88
212
  end
89
213
 
90
214
  end
215
+
91
216
  end
92
217
 
@@ -12,9 +12,10 @@ module Perry::Adapters
12
12
  end
13
13
 
14
14
  def read(options)
15
- log(options, "RPC #{config[:service]}") {
15
+ query = options[:relation].to_hash
16
+ log(query, "RPC #{config[:service]}") {
16
17
  self.service.call.send(self.namespace).send(self.service_name,
17
- options.merge(config[:default_options] || {}))
18
+ query.merge(config[:default_options] || {}))
18
19
  }
19
20
  end
20
21
 
@@ -5,20 +5,22 @@ module Perry::Adapters
5
5
  class RestfulHTTPAdapter < Perry::Adapters::AbstractAdapter
6
6
  register_as :restful_http
7
7
 
8
- attr_reader :last_response
9
-
10
- def initialize(*args)
11
- super
12
- @configuration_contexts << { :primary_key => :id }
8
+ class KeyError < Perry::PerryError
9
+ def message
10
+ '(restful_http_adapter) request not sent because primary key value was nil'
11
+ end
13
12
  end
14
13
 
15
- def write(object)
14
+ attr_reader :last_response
15
+
16
+ def write(options)
17
+ object = options[:object]
16
18
  params = build_params_from_attributes(object)
17
19
  object.new_record? ? post_http(object, params) : put_http(object, params)
18
20
  end
19
21
 
20
- def delete(object)
21
- delete_http(object)
22
+ def delete(options)
23
+ delete_http(options[:object])
22
24
  end
23
25
 
24
26
  protected
@@ -55,7 +57,19 @@ module Perry::Adapters
55
57
  self.log(params, "#{method.to_s.upcase} #{req_uri}") do
56
58
  @last_response = Net::HTTP.new(req_uri.host, req_uri.port).start { |http| http.request(request) }
57
59
  end
58
- parse_response_code(@last_response)
60
+
61
+ Perry::Persistence::Response.new.tap do |response|
62
+ response.status = @last_response.code.to_i if @last_response
63
+ response.success = parse_response_code(@last_response)
64
+ response.meta = http_headers(@last_response).to_hash if @last_response
65
+ response.raw = @last_response.body if @last_response
66
+ response.raw_format = config[:format] ? config[:format].gsub(/\W/, '').to_sym : nil
67
+ end
68
+ rescue KeyError => ex
69
+ Perry::Persistence::Response.new.tap do |response|
70
+ response.success = false
71
+ response.parsed = { :base => ex.message }
72
+ end
59
73
  end
60
74
 
61
75
  def parse_response_code(response)
@@ -69,7 +83,8 @@ module Perry::Adapters
69
83
 
70
84
  def build_params_from_attributes(object)
71
85
  if self.config[:post_body_wrapper]
72
- params = self.config[:default_options] || {}
86
+ defaults = self.config[:default_options]
87
+ params = defaults ? defaults.dup : {}
73
88
  params.merge!(object.write_options[:default_options]) if object.write_options.is_a?(Hash) && object.write_options[:default_options].is_a?(Hash)
74
89
 
75
90
  object.attributes.each do |attribute, value|
@@ -84,7 +99,11 @@ module Perry::Adapters
84
99
 
85
100
  def build_uri(object, method)
86
101
  url = [self.config[:host].gsub(%r{/$}, ''), self.config[:service]]
87
- url << object.send(self.config[:primary_key]) unless object.new_record?
102
+ unless object.new_record?
103
+ primary_key = self.config[:primary_key] || object.primary_key
104
+ pk_value = object.send(primary_key) or raise KeyError
105
+ url << pk_value
106
+ end
88
107
  uri = URI.parse "#{url.join('/')}#{self.config[:format]}"
89
108
 
90
109
  # TRP: method DELETE has no POST body so we have to append any default options onto the query string
@@ -95,5 +114,11 @@ module Perry::Adapters
95
114
  uri
96
115
  end
97
116
 
117
+ def http_headers(response)
118
+ response.to_hash.inject({}) do |clean_headers, (key, values)|
119
+ clean_headers.merge(key => values.length > 1 ? values : values.first)
120
+ end
121
+ end
122
+
98
123
  end
99
124
  end
@@ -43,8 +43,14 @@ module Perry::Association
43
43
  raise NotImplementedError, "You must define collection? in subclasses."
44
44
  end
45
45
 
46
- def primary_key
47
- options[:primary_key] || :id
46
+ def primary_key(object=nil)
47
+ if options[:primary_key]
48
+ options[:primary_key]
49
+ elsif is_a?(BelongsTo)
50
+ target_klass(object).primary_key
51
+ elsif is_a?(Has)
52
+ source_klass.primary_key
53
+ end
48
54
  end
49
55
 
50
56
  def foreign_key
@@ -52,10 +58,18 @@ module Perry::Association
52
58
  end
53
59
 
54
60
  def target_klass(object=nil)
61
+ eager_loading = object.is_a?(Array)
55
62
  if options[:polymorphic] && object
56
63
  poly_type = object.is_a?(Perry::Base) ? object.send("#{id}_type") : object
57
64
  end
58
65
 
66
+ # This is an eager loading attempt
67
+ if eager_loading && !eager_loadable?
68
+ raise(Perry::AssociationPreloadNotSupported,
69
+ "This association cannot be eager loaded. It has config with procs or it is a " +
70
+ "polymorphic belongs_to association.")
71
+ end
72
+
59
73
  klass = if poly_type
60
74
  type_string = [
61
75
  options[:polymorphic_namespace],
@@ -92,9 +106,10 @@ module Perry::Association
92
106
  # TRP: Only eager loadable if association query does not depend on instance
93
107
  # data
94
108
  def eager_loadable?
95
- Perry::Relation::FINDER_OPTIONS.inject(true) do |condition, key|
96
- condition && !options[key].respond_to?(:call)
109
+ dynamic_config = Perry::Relation::FINDER_OPTIONS.inject(false) do |condition, key|
110
+ condition || options[key].respond_to?(:call)
97
111
  end
112
+ !dynamic_config && !(self.polymorphic? && self.is_a?(BelongsTo))
98
113
  end
99
114
 
100
115
  protected
@@ -113,7 +128,8 @@ module Perry::Association
113
128
 
114
129
  # TRP: Make sure the value looks like a variable syntaxtually
115
130
  def sanitize_type_attribute(string)
116
- string.gsub(/[^a-zA-Z]\w*/, '')
131
+ string =~ /^[a-zA-Z]\w*/
132
+ Regexp.last_match.to_s
117
133
  end
118
134
 
119
135
  end
@@ -154,8 +170,15 @@ module Perry::Association
154
170
  # where(:id => source[:foo_id])
155
171
  #
156
172
  def scope(object)
157
- if object[self.foreign_key]
158
- base_scope(object).where(self.primary_key => object[self.foreign_key])
173
+ if object.is_a? Array
174
+ keys = object.collect(&self.foreign_key.to_sym)
175
+ keys = nil if keys.empty?
176
+ else
177
+ keys = object[self.foreign_key]
178
+ end
179
+ if keys
180
+ scope = base_scope(object)
181
+ scope.where(self.primary_key(object) => keys)
159
182
  end
160
183
  end
161
184
 
@@ -192,13 +215,19 @@ module Perry::Association
192
215
  # where(:parent_id => source[:id], :parent_type => source.class)
193
216
  #
194
217
  def scope(object)
195
- return nil unless object[self.primary_key]
196
- s = base_scope(object).where(self.foreign_key => object[self.primary_key])
197
- if polymorphic?
198
- s = s.where(
199
- polymorphic_type => Perry::Base.base_class_name(object.class) )
218
+ if object.is_a? Array
219
+ keys = object.collect(&self.primary_key.to_sym)
220
+ keys = nil if keys.empty?
221
+ klass = object.first.class
222
+ else
223
+ keys = object[self.primary_key]
224
+ klass = object.class
225
+ end
226
+ if keys
227
+ scope = base_scope(object).where(self.foreign_key => keys)
228
+ scope = scope.where(polymorphic_type => Perry::Base.base_class_name(klass)) if polymorphic?
229
+ scope
200
230
  end
201
- s
202
231
  end
203
232
 
204
233
  def polymorphic_type
@@ -288,7 +317,7 @@ module Perry::Association
288
317
  else
289
318
  relation = relation.where(
290
319
  proc do
291
- { target_association.primary_key => proxy_ids.call }
320
+ { target_association.primary_key(options[:source_type]) => proxy_ids.call }
292
321
  end
293
322
  )
294
323
  end
data/lib/perry/base.rb CHANGED
@@ -2,28 +2,29 @@
2
2
  require 'perry/errors'
3
3
  require 'perry/associations/contains'
4
4
  require 'perry/associations/external'
5
- require 'perry/association_preload'
6
- require 'perry/cacheable'
7
5
  require 'perry/serialization'
8
6
  require 'perry/relation'
9
7
  require 'perry/scopes'
10
8
  require 'perry/adapters'
11
-
9
+ require 'perry/middlewares'
10
+ require 'perry/processors'
12
11
 
13
12
  class Perry::Base
14
13
  include Perry::Associations::Contains
15
14
  include Perry::Associations::External
16
- include Perry::AssociationPreload
17
15
  include Perry::Serialization
18
16
  include Perry::Scopes
19
17
 
20
- attr_accessor :attributes, :new_record, :read_options, :write_options
18
+ DEFAULT_PRIMARY_KEY = :id
19
+
20
+ attr_accessor :attributes, :new_record, :saved, :read_options, :write_options
21
21
  alias :new_record? :new_record
22
+ alias :saved? :saved
23
+ alias :persisted? :saved?
22
24
 
23
- class_inheritable_accessor :read_adapter, :write_adapter, :cacheable,
25
+ class_inheritable_accessor :read_adapter, :write_adapter,
24
26
  :defined_attributes, :scoped_methods, :defined_associations
25
27
 
26
- self.cacheable = false
27
28
  self.defined_associations = {}
28
29
  self.defined_attributes = []
29
30
 
@@ -36,6 +37,14 @@ class Perry::Base
36
37
  @attributes[attribute.to_s]
37
38
  end
38
39
 
40
+ def errors
41
+ @errors ||= {}
42
+ end
43
+
44
+ def primary_key
45
+ self.class.primary_key
46
+ end
47
+
39
48
  protected
40
49
 
41
50
  # TRP: Common interface for setting attributes to keep things consistent; if
@@ -61,7 +70,22 @@ class Perry::Base
61
70
 
62
71
  delegate :find, :first, :all, :search, :apply_finder_options, :to => :scoped
63
72
  delegate :select, :group, :order, :joins, :where, :having, :limit, :offset,
64
- :from, :fresh, :to => :scoped
73
+ :from, :includes, :to => :scoped
74
+ delegate :modifiers, :to => :scoped
75
+
76
+ def primary_key
77
+ @primary_key || DEFAULT_PRIMARY_KEY
78
+ end
79
+
80
+ # Allows you to specify an attribute other than :id to use as your models
81
+ # primary key.
82
+ #
83
+ def set_primary_key(attribute)
84
+ unless defined_attributes.include?(attribute.to_s)
85
+ raise Perry::PerryError.new("cannot set primary key to non-existent attribute")
86
+ end
87
+ @primary_key = attribute.to_sym
88
+ end
65
89
 
66
90
  def new_from_data_store(hash)
67
91
  if hash.nil?
@@ -96,8 +120,7 @@ class Perry::Base
96
120
  protected
97
121
 
98
122
  def fetch_records(relation)
99
- options = relation.to_hash
100
- self.read_adapter.read(options).collect { |hash| self.new_from_data_store(hash) }.compact.tap { |result| eager_load_associations(result, relation) }
123
+ self.read_adapter.call(:read, :relation => relation).compact
101
124
  end
102
125
 
103
126
  def read_with(adapter_type)
@@ -136,13 +159,6 @@ class Perry::Base
136
159
  end
137
160
  end
138
161
 
139
- def configure_cacheable(options={})
140
- unless cacheable
141
- self.send(:include, Perry::Cacheable)
142
- self.enable_caching(options)
143
- end
144
- end
145
-
146
162
  # TRP: Used to declare attributes -- only attributes that are declared will be available
147
163
  def attributes(*attributes)
148
164
  return self.defined_attributes if attributes.empty?
@@ -1,18 +1,18 @@
1
- module Perry::Cacheable
2
-
1
+ class Perry::Middlewares::CacheRecords
2
+
3
3
  class Entry
4
-
4
+
5
5
  attr_accessor :value, :expire_at
6
-
6
+
7
7
  def initialize(value, expire_at)
8
8
  self.value = value
9
9
  self.expire_at = expire_at
10
10
  end
11
-
11
+
12
12
  def expired?
13
13
  Time.now > self.expire_at rescue true
14
14
  end
15
-
15
+
16
16
  end
17
-
17
+
18
18
  end
@@ -0,0 +1,18 @@
1
+ module Perry::Middlewares::CacheRecords::Scopes
2
+ def self.included(model)
3
+ model.class_eval do
4
+
5
+ # Using the :fresh scope will skip the cache and execute query regardless of
6
+ # whether a cached result is available or not.
7
+ scope :fresh, (lambda do |*args|
8
+ val = args.first.nil? ? true : args.first
9
+ modifiers(:fresh => val)
10
+ end)
11
+
12
+ # Using the :reset_cache scope in a query will delete all entries from the
13
+ # cache store before running the query. Whenever possible, you should use
14
+ # the :fresh scope instead of :reset_cache.
15
+ scope :reset_cache, modifiers(:reset_cache => true)
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,4 @@
1
- module Perry::Cacheable
1
+ class Perry::Middlewares::CacheRecords
2
2
 
3
3
  class Store
4
4