memory_model 0.0.2 → 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 (70) hide show
  1. checksums.yaml +15 -0
  2. data/.ruby-version +1 -0
  3. data/.travis.yml +7 -4
  4. data/Appraisals +9 -0
  5. data/Guardfile +7 -1
  6. data/README.md +5 -1
  7. data/Rakefile +12 -0
  8. data/gemfiles/rails_3.gemfile +8 -0
  9. data/gemfiles/rails_3.gemfile.lock +109 -0
  10. data/gemfiles/rails_4.gemfile +8 -0
  11. data/gemfiles/rails_4.gemfile.lock +117 -0
  12. data/lib/memory_model/base/actions/class_methods.rb +27 -0
  13. data/lib/memory_model/base/actions.rb +75 -0
  14. data/lib/memory_model/base/attributes.rb +87 -0
  15. data/lib/memory_model/base/auto_increment.rb +47 -0
  16. data/lib/memory_model/base/collectible.rb +26 -0
  17. data/lib/memory_model/base/conversion.rb +11 -0
  18. data/lib/memory_model/base/fields/field.rb +57 -0
  19. data/lib/memory_model/base/fields/field_set.rb +87 -0
  20. data/lib/memory_model/base/fields.rb +49 -0
  21. data/lib/memory_model/base/operations/comparisons.rb +25 -0
  22. data/lib/memory_model/base/operations.rb +10 -0
  23. data/lib/memory_model/base/persistence.rb +13 -11
  24. data/lib/memory_model/base.rb +58 -43
  25. data/lib/memory_model/collection/finders.rb +75 -0
  26. data/lib/memory_model/collection/index/multi.rb +61 -0
  27. data/lib/memory_model/collection/index/unique.rb +79 -0
  28. data/lib/memory_model/collection/index.rb +86 -0
  29. data/lib/memory_model/collection/initializers.rb +48 -0
  30. data/lib/memory_model/collection/loader_delegate.rb +63 -0
  31. data/lib/memory_model/collection/marshaled_record.rb +23 -0
  32. data/lib/memory_model/collection/operations.rb +82 -0
  33. data/lib/memory_model/collection.rb +18 -73
  34. data/lib/memory_model/version.rb +1 -1
  35. data/lib/memory_model.rb +7 -7
  36. data/memory_model.gemspec +8 -3
  37. data/spec/benchmark/benchmark.rb +126 -0
  38. data/spec/memory_model/base/{actionable_spec.rb → actions_spec.rb} +34 -103
  39. data/spec/memory_model/base/{attributable_spec.rb → attributes_spec.rb} +4 -6
  40. data/spec/memory_model/base/{collectable_spec.rb → collectible_spec.rb} +1 -1
  41. data/spec/memory_model/base/fieldable/field_set_spec.rb +23 -37
  42. data/spec/memory_model/base/fieldable/field_spec.rb +16 -16
  43. data/spec/memory_model/base/{fieldable_spec.rb → fields_spec.rb} +1 -1
  44. data/spec/memory_model/base/{comparable_spec.rb → operations/comparisons_spec.rb} +4 -4
  45. data/spec/memory_model/base/persistence_spec.rb +2 -2
  46. data/spec/memory_model/base_spec.rb +10 -9
  47. data/spec/memory_model/collection_spec.rb +24 -146
  48. data/spec/spec_helper.rb +11 -0
  49. data/spec/support/delegate_matcher.rb +40 -0
  50. metadata +120 -65
  51. data/.idea/.rakeTasks +0 -7
  52. data/.idea/dictionaries/jwaldrip.xml +0 -3
  53. data/.idea/encodings.xml +0 -5
  54. data/.idea/memory_model.iml +0 -47
  55. data/.idea/misc.xml +0 -8
  56. data/.idea/modules.xml +0 -9
  57. data/.idea/scopes/scope_settings.xml +0 -5
  58. data/.idea/vcs.xml +0 -7
  59. data/.idea/workspace.xml +0 -701
  60. data/lib/memory_model/base/actionable.rb +0 -95
  61. data/lib/memory_model/base/attributable.rb +0 -76
  62. data/lib/memory_model/base/collectable.rb +0 -22
  63. data/lib/memory_model/base/comparable.rb +0 -16
  64. data/lib/memory_model/base/fieldable/field.rb +0 -35
  65. data/lib/memory_model/base/fieldable/field_set.rb +0 -74
  66. data/lib/memory_model/base/fieldable.rb +0 -45
  67. data/lib/memory_model/base/versionable.rb +0 -17
  68. data/lib/memory_model/core_ext/object.rb +0 -5
  69. data/spec/memory_model/base/versionable_spec.rb +0 -31
  70. data/spec/memory_model/core_ext/object_spec.rb +0 -12
@@ -0,0 +1,87 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'active_support/core_ext/string'
4
+ require 'set'
5
+
6
+ module MemoryModel
7
+ class Base
8
+ module Fields
9
+ class FieldSet < Set
10
+
11
+ def [](name)
12
+ find { |f| f.name == name.to_sym }
13
+ end
14
+
15
+ def add(name, options={})
16
+ delete_if { |f| f == name }
17
+ self << Field.new(name, options)
18
+ end
19
+
20
+ def include?(name)
21
+ self[name].present?
22
+ end
23
+
24
+ def comparable
25
+ select(&:comparable?).map(&:to_sym)
26
+ end
27
+
28
+ def set_default_values(model, attributes={})
29
+ model.attributes = self.map(&:name).reduce(attributes.with_indifferent_access) do |hash, field|
30
+ hash[field] ||= fetch_default_value(model, field)
31
+ hash
32
+ end
33
+ end
34
+
35
+ def set_default_value(model, field)
36
+ model.write_attribute field, fetch_default_value(model, field)
37
+ end
38
+
39
+ def fetch_default_value(model, field)
40
+ default = self[field].default
41
+ send("fetch_value_using_#{default.class.name.underscore}", model, default)
42
+ rescue NoMethodError => e
43
+ raise ArgumentError, "#{default} must be a string, symbol, lambda or proc"
44
+ end
45
+
46
+ private
47
+
48
+ def fetch_value_using_proc(model, proc)
49
+ raise TypeError, 'value must be a Proc' unless proc.is_a? Proc
50
+ if proc.lambda? && proc.arity == 0
51
+ proc.call
52
+ elsif proc.arity < 1
53
+ model.instance_eval(&proc)
54
+ elsif proc.arity == 1
55
+ proc.yield model
56
+ else
57
+ raise ArgumentError, "#{proc} must have an arity of 0..1, got #{proc.arity}"
58
+ end
59
+ end
60
+
61
+ def fetch_value_using_string(model, string)
62
+ raise TypeError, 'value must be a String' unless string.is_a? String
63
+ string
64
+ end
65
+
66
+ def fetch_value_using_symbol(model, symbol)
67
+ raise TypeError, 'value must be a Symbol' unless symbol.is_a? Symbol
68
+ model.instance_eval(&symbol)
69
+ end
70
+
71
+ def fetch_value_using_nil_class(model, nil_object)
72
+ raise TypeError, 'value must be a NilClass' unless nil_object.is_a? NilClass
73
+ nil
74
+ end
75
+
76
+ def method_missing(m, *args, &block)
77
+ if to_a.respond_to? m
78
+ to_a.send m, *args, &block
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/dependencies/autoload'
3
+ require 'securerandom'
4
+
5
+ module MemoryModel
6
+ class Base
7
+ module Fields
8
+
9
+ extend ConcernedInheritance
10
+ extend ActiveSupport::Concern
11
+ extend ActiveSupport::Autoload
12
+ include ActiveModel::AttributeMethods
13
+
14
+ autoload :FieldSet
15
+ autoload :Field
16
+
17
+ inherited do
18
+ instance_variable_set :@fields, baseclass.fields
19
+ end
20
+
21
+ module ClassMethods
22
+ def field(attr, options={})
23
+ define_attribute_method attr unless instance_method_already_implemented? attr
24
+ fields.add(attr.to_sym, options)
25
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
26
+ def #{attr}
27
+ read_attribute :#{attr}
28
+ end
29
+
30
+ def #{attr}=(value)
31
+ write_attribute :#{attr}, value
32
+ end
33
+ RUBY
34
+ end
35
+
36
+ def fields
37
+ return nil if self == MemoryModel::Base
38
+ @fields ||= FieldSet.new
39
+ end
40
+
41
+ end
42
+
43
+ def fields
44
+ self.class.fields
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/hash_with_indifferent_access'
3
+
4
+ module MemoryModel
5
+ class Base
6
+ module Operations
7
+ module Comparisons
8
+
9
+ def ==(other_object)
10
+ attributes.slice(*fields.comparable) ==
11
+ other_object.to_hash.with_indifferent_access.slice(*fields.comparable)
12
+ end
13
+
14
+ def !=(other_object)
15
+ !(self == other_object)
16
+ end
17
+
18
+ def ===(other_object)
19
+ other_object.kind_of?(self.class) && self == other_object
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module MemoryModel
2
+ class Base
3
+ module Operations
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Comparisons
7
+
8
+ end
9
+ end
10
+ end
@@ -1,15 +1,17 @@
1
- module MemoryModel::Base::Persistence
1
+ module MemoryModel
2
+ class Base
3
+ module Persistence
2
4
 
3
- def persisted?
4
- !!self.class.find(self.id)
5
- rescue MemoryModel::RecordNotFoundError
6
- false
7
- end
5
+ def persisted?
6
+ !!self.class.find_by(_uuid_: self._uuid_)
7
+ end
8
8
 
9
- alias :exists? :persisted?
9
+ alias :exists? :persisted?
10
10
 
11
- def new_record?
12
- !persisted?
13
- end
11
+ def new_record?
12
+ !persisted?
13
+ end
14
14
 
15
- end
15
+ end
16
+ end
17
+ end
@@ -1,51 +1,66 @@
1
- require "concerned_inheritance"
1
+ require 'concerned_inheritance'
2
2
  require 'active_support/core_ext/object'
3
3
  require 'active_support/core_ext/hash'
4
4
  require 'active_support/dependencies/autoload'
5
- require 'active_support/core_ext/hash/indifferent_access'
6
5
  require 'active_model'
7
6
 
8
- class MemoryModel::Base
9
- extend ActiveSupport::Autoload
10
- extend ConcernedInheritance
11
-
12
- autoload :Fieldable
13
- autoload :Collectable
14
- autoload :Comparable
15
- autoload :Actionable
16
- autoload :Attributable
17
- autoload :Versionable
18
- autoload :Persistence
19
-
20
- # Active Model Additions
21
- extend ActiveModel::Callbacks
22
- extend ActiveModel::Naming
23
- extend ActiveModel::Translation
24
- include ActiveModel::Conversion
25
- include ActiveModel::MassAssignmentSecurity
26
- include ActiveModel::Observing
27
- include ActiveModel::Serialization
28
- include ActiveModel::Validations
29
-
30
- # Memory Model Additions
31
- include Fieldable
32
- include Collectable
33
- include Comparable
34
- include Actionable
35
- include Attributable
36
- include Versionable
37
- include Persistence
38
-
39
- # Active Model Callbacks
40
- define_model_callbacks :initialize, only: [:after]
41
-
42
- def initialize(attributes={ })
43
- unless self.class.collection.is_a? MemoryModel::Collection
44
- raise MemoryModel::InvalidCollectionError, "#{self.class} does not have an assigned collection"
7
+ module MemoryModel
8
+
9
+ class InvalidCollectionError < Error ; end
10
+
11
+ class Base
12
+ extend ActiveSupport::Autoload
13
+ extend ConcernedInheritance
14
+
15
+ autoload :Fields
16
+ autoload :Collectible
17
+ autoload :Comparison
18
+ autoload :Actions
19
+ autoload :Attributes
20
+ autoload :Persistence
21
+ autoload :Operations
22
+ autoload :Conversion
23
+ autoload :AutoIncrement
24
+
25
+ # Active Model Additions
26
+ extend ActiveModel::Callbacks
27
+ extend ActiveModel::Naming
28
+ extend ActiveModel::Translation
29
+ include ActiveModel::Conversion
30
+ include ActiveModel::Serialization
31
+ include ActiveModel::Validations
32
+
33
+ # 3.2 Only Active Model Additions
34
+ if ActiveModel::VERSION::MAJOR < 4 || (ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR > 2)
35
+ include ActiveModel::MassAssignmentSecurity
36
+ include ActiveModel::Observing
45
37
  end
46
- @attributes = fields.default_values(self, attributes).with_indifferent_access
47
- @deleted = false
48
- run_callbacks :initialize
49
- end
50
38
 
39
+ # Memory Model Additions
40
+ include Fields
41
+ include Collectible
42
+ include Operations::Comparisons
43
+ include Actions
44
+ include Attributes
45
+ include Persistence
46
+ include Conversion
47
+ include AutoIncrement
48
+
49
+ # Active Model Callbacks
50
+ define_model_callbacks :initialize, only: [:after]
51
+
52
+ def initialize(attributes={})
53
+ unless self.class.collection.is_a? MemoryModel::Collection
54
+ raise MemoryModel::InvalidCollectionError, "#{self.class} does not have an assigned collection"
55
+ end
56
+ fields.set_default_values(self, attributes)
57
+ run_callbacks :initialize
58
+ end
59
+
60
+ def initialize_dup(other)
61
+ @attributes = other.attributes.dup
62
+ reset_incremented_fields!
63
+ end
64
+
65
+ end
51
66
  end
@@ -0,0 +1,75 @@
1
+ module MemoryModel
2
+
3
+ class RecordNotFoundError < Error;
4
+ end
5
+
6
+ class Collection
7
+ module Finders
8
+
9
+ def all
10
+ LoaderDelegate.new records
11
+ end
12
+
13
+ def count
14
+ _uuids_.count
15
+ end
16
+
17
+ def find(key)
18
+ read(key).load
19
+ rescue NoMethodError
20
+ raise RecordNotFoundError
21
+ end
22
+
23
+ def find_all(*ids)
24
+ read_all(*ids).map(&:load)
25
+ end
26
+
27
+ def find_by(hash)
28
+ where(hash).first
29
+ end
30
+
31
+ def find_or_initialize_by(hash)
32
+ find_by(hash) || model.new(hash)
33
+ end
34
+
35
+ def find_or_create_by(hash)
36
+ find_by(hash) || model.create(hash)
37
+ end
38
+
39
+ def find_or_create_by!(hash)
40
+ find_by(hash) || model.create!(hash)
41
+ end
42
+
43
+ def where(hash)
44
+ matched_ids = hash.symbolize_keys.reduce(_uuids_) do |array, (attr, value)|
45
+ records = if indexes.has_key?(attr)
46
+ where_in_index(attr, value).compact.map(&:uuid)
47
+ else
48
+ where_in_all(attr, value).map(&:_uuid_)
49
+ end
50
+ array & records
51
+ end
52
+ load_all(*matched_ids)
53
+ end
54
+
55
+ private
56
+
57
+ def _uuids_
58
+ indexes[:_uuid_].keys
59
+ end
60
+
61
+ def records
62
+ indexes[:_uuid_].values
63
+ end
64
+
65
+ def where_in_all(attr, value)
66
+ all.select { |record| record.read_attribute(attr) == value }
67
+ end
68
+
69
+ def where_in_index(attr, value)
70
+ indexes[attr].where value
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ module MemoryModel
2
+ class Collection
3
+ class Index
4
+ class Multi < MemoryModel::Collection::Index
5
+
6
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
7
+ # exists in the index.
8
+ def create(key, item)
9
+ insert_into_key(key, item)
10
+ end
11
+
12
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
13
+ def update(key, item)
14
+ raise(RecordNotInIndexError, [self, item]) unless exists? item
15
+ delete(item)
16
+ create(key, item)
17
+ end
18
+
19
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
20
+ def read(key)
21
+ index[key].first
22
+ end
23
+
24
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
25
+ def delete(key)
26
+ index.values.map { |refs| refs.delete key }
27
+ end
28
+
29
+ # `exists?` return whether or not an item with the given storage id exists.
30
+ def exists?(item)
31
+ index.values.any? { |refs| refs.has_key? item.uuid }
32
+ end
33
+
34
+ # `values` should return the values of the index
35
+ def values
36
+ index.values.map(&:values)
37
+ end
38
+
39
+ private
40
+
41
+ def where_using_default(matcher)
42
+ Array.wrap index[matcher].try(:values)
43
+ end
44
+
45
+ def where_using_proc(matcher)
46
+ index.slice(*index.keys.select(&matcher)).values.map(&:values).flatten
47
+ end
48
+
49
+ def where_using_regexp(matcher)
50
+ where_using_proc ->(key) { key =~ matcher }
51
+ end
52
+
53
+ def insert_into_key(key, item)
54
+ index[key] ||= {}
55
+ index[key][item.uuid] = item
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ module MemoryModel
2
+
3
+ class NilValueError < IndexError
4
+
5
+ def initialize(index)
6
+ super "`#{index.name}` cannot be nil"
7
+ end
8
+
9
+ end
10
+
11
+ class RecordNotUniqueError < IndexError
12
+
13
+ def initialize(args)
14
+ index, key = args
15
+ super "`#{index.name}` with `#{key}` already exists"
16
+ end
17
+
18
+ end
19
+
20
+ class Collection
21
+ class Index
22
+ class MemoryModel::Collection::Index::Unique < MemoryModel::Collection::Index
23
+
24
+ delegate :values_at, to: :index
25
+
26
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
27
+ # exists in the index.
28
+ def create(key, item)
29
+ raise(NilValueError, self) if key.nil? && !options[:allow_nil]
30
+ raise(RecordNotUniqueError, [self, key]) if index.has_key?(key)
31
+ return if key.nil?
32
+ index[key] = item
33
+ end
34
+
35
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
36
+ def update(key, item)
37
+ raise(RecordNotInIndexError, [self, item]) unless exists? item
38
+ delete(item.uuid)
39
+ create(key, item)
40
+ end
41
+
42
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
43
+ def read(key)
44
+ index[key]
45
+ end
46
+
47
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
48
+ def delete(key)
49
+ index.delete_if { |k, value| key == value.uuid }
50
+ end
51
+
52
+ # `exists?` return whether or not an item with the given storage id exists.
53
+ def exists?(item)
54
+ index.any? { |key, value| item.uuid == value.uuid }
55
+ end
56
+
57
+ # `values` should return the values of the index
58
+ def values
59
+ index.values
60
+ end
61
+
62
+ private
63
+
64
+ def where_using_default(matcher)
65
+ [read(matcher)]
66
+ end
67
+
68
+ def where_using_proc(matcher)
69
+ index.values_at *index.keys.select(&matcher)
70
+ end
71
+
72
+ def where_using_regexp(matcher)
73
+ where_using_proc ->(key) { key =~ matcher }
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,86 @@
1
+ module MemoryModel
2
+
3
+ class IndexError < Error
4
+ end
5
+
6
+ class InvalidWhereQuery < IndexError
7
+
8
+ def initialize(matcher_class)
9
+ super "Unable to perform a where with #{matcher_class}"
10
+ end
11
+
12
+ end
13
+
14
+ class RecordNotInIndexError < IndexError
15
+
16
+ def initialize(args)
17
+ item, index = args
18
+ super "record `#{item.uuid}` is missing from index `#{index.name}`"
19
+ end
20
+
21
+ end
22
+
23
+ class Collection
24
+ class Index
25
+ extend ActiveSupport::Autoload
26
+
27
+ autoload :Unique
28
+ autoload :Multi
29
+
30
+ attr_reader :name, :options, :index
31
+
32
+ delegate :clear, :keys, to: :index
33
+ delegate :count, to: :values
34
+
35
+ def initialize(name, options)
36
+ @name = name
37
+ @options = options
38
+ @index = {}
39
+ end
40
+
41
+ # This is the base index, each method below must be implemented on each subclass
42
+
43
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
44
+ # exists in the index.
45
+ def create(key, item)
46
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
47
+ end
48
+
49
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
50
+ def update(key, item)
51
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
52
+ end
53
+
54
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
55
+ def read(key)
56
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
57
+ end
58
+
59
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
60
+ def delete(key)
61
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
62
+ end
63
+
64
+ # `exists?` return whether or not an item with the given storage id exists.
65
+ def exists?(item)
66
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
67
+ end
68
+
69
+ # `where` should allow me to specify complex arguments and return an array
70
+ def where(matcher)
71
+ matcher_class = matcher.class.name.underscore
72
+ send("where_using_#{matcher_class}", matcher)
73
+ rescue NoMethodError
74
+ respond_to?(:where_using_default, true) ? where_using_default(matcher) :
75
+ raise(InvalidWhereQuery, matcher_class)
76
+ end
77
+
78
+ # `values` should return the values of the index
79
+ def values
80
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,48 @@
1
+ module MemoryModel
2
+ class Collection
3
+ module Initializers
4
+ extend ActiveSupport::Concern
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ set_primary_key :_uuid_, default: nil
9
+ end
10
+
11
+ def add_index(name, options={})
12
+ type = :unique if options.delete(:unique)
13
+ type ||= options.delete(:type) || :multi
14
+ indexes[name] = Index.const_get(type.to_s.camelize).new(name, options)
15
+ rescue NameError => e
16
+ raise TypeError, "#{type.inspect} is not a valid index"
17
+ end
18
+
19
+ def indexes
20
+ @indexes ||= {}
21
+ end
22
+
23
+ def index_names
24
+ indexes.keys
25
+ end
26
+
27
+ def set_primary_key(key, options={})
28
+ if options[:auto_increment] != false && !options.has_key?(:default)
29
+ options[:auto_increment] = true
30
+ end
31
+ options[:comparable] ||= false
32
+ @model.field key, options
33
+ add_index key, type: :unique
34
+ @primary_key = key
35
+ end
36
+
37
+ module ClassMethods
38
+
39
+ def all
40
+ MemoryModel::Collection.instance_variable_get(:@all) ||
41
+ MemoryModel::Collection.instance_variable_set(:@all, [])
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end