cistern 2.3.0 → 2.4.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,24 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "json", "~> 1.8"
7
+
8
+ group :test do
9
+ gem "guard-rspec", "~> 4.2", :require => false
10
+ gem "guard-bundler", "~> 2.0", :require => false
11
+ gem "pry-nav"
12
+ gem "rake"
13
+ gem "rspec", "~> 3.3"
14
+ gem "listen", "~> 3.0.5"
15
+ gem "redis-namespace", "~> 1.4", "< 1.5"
16
+ gem "codeclimate-test-reporter", :require => false
17
+ end
18
+
19
+ group :formatters do
20
+ gem "formatador"
21
+ gem "awesome_print"
22
+ end
23
+
24
+ gemspec :path => "../"
@@ -10,6 +10,7 @@ module Cistern
10
10
  Timeout = Class.new(Error)
11
11
 
12
12
  require 'cistern/hash'
13
+ require 'cistern/hash_support'
13
14
  require 'cistern/string'
14
15
  require 'cistern/mock'
15
16
  require 'cistern/wait_for'
@@ -2,57 +2,36 @@ module Cistern::Attributes
2
2
  PROTECTED_METHODS = [:cistern, :service, :identity, :collection].freeze
3
3
  TRUTHY = ['true', '1'].freeze
4
4
 
5
- def self.parsers
6
- @parsers ||= {
7
- array: ->(v, _) { [*v] },
8
- boolean: ->(v, _) { TRUTHY.include?(v.to_s.downcase) },
9
- float: ->(v, _) { v && v.to_f },
10
- integer: ->(v, _) { v && v.to_i },
11
- string: ->(v, opts) { (opts[:allow_nil] && v.nil?) ? v : v.to_s },
12
- time: ->(v, _) { v.is_a?(Time) ? v : v && Time.parse(v.to_s) },
13
- }
14
- end
5
+ module ClassMethods
6
+ def parsers
7
+ @parsers ||= {
8
+ array: ->(v, _) { [*v] },
9
+ boolean: ->(v, _) { TRUTHY.include?(v.to_s.downcase) },
10
+ float: ->(v, _) { v && v.to_f },
11
+ integer: ->(v, _) { v && v.to_i },
12
+ string: ->(v, _) { v && v.to_s },
13
+ time: ->(v, _) { v.is_a?(Time) ? v : v && Time.parse(v.to_s) },
14
+ }
15
+ end
15
16
 
16
- def self.transforms
17
- @transforms ||= {
18
- squash: proc do |_k, _v, options|
19
- v = Cistern::Hash.stringify_keys(_v)
20
- squash = options[:squash]
21
-
22
- if v.is_a?(::Hash) && squash.is_a?(Array)
23
- travel = lambda do |tree, path|
24
- if tree.is_a?(::Hash)
25
- travel.call(tree[path.shift], path)
26
- else
27
- tree
28
- end
29
- end
17
+ def squasher(tree, path)
18
+ tree.is_a?(::Hash) ? squasher(tree[path.shift], path) : tree
19
+ end
30
20
 
31
- travel.call(v, squash.dup)
32
- elsif v.is_a?(::Hash)
33
- squash_s = squash.to_s
21
+ def transforms
22
+ @transforms ||= {
23
+ squash: proc do |_, _v, options|
24
+ v = Cistern::Hash.stringify_keys(_v)
25
+ squash = options[:squash]
34
26
 
35
- if v.key?(key = squash_s.to_sym)
36
- v[key]
37
- elsif v.key?(squash_s)
38
- v[squash_s]
39
- else
40
- v
41
- end
42
- else v
43
- end
44
- end,
45
- none: ->(_, v, _) { v }
46
- }
47
- end
48
-
49
- def self.default_parser
50
- @default_parser ||= ->(v, _opts) { v }
51
- end
27
+ v.is_a?(::Hash) ? squasher(v, squash.dup) : v
28
+ end,
29
+ none: ->(_, v, _) { v }
30
+ }
31
+ end
52
32
 
53
- module ClassMethods
54
- def _load(marshalled)
55
- new(Marshal.load(marshalled))
33
+ def default_parser
34
+ @default_parser ||= ->(v, _opts) { v }
56
35
  end
57
36
 
58
37
  def aliases
@@ -60,47 +39,24 @@ module Cistern::Attributes
60
39
  end
61
40
 
62
41
  def attributes
63
- @attributes ||= {}
42
+ @attributes ||= parent_attributes || {}
64
43
  end
65
44
 
66
- def attribute(_name, options = {})
67
- if defined? Cistern::Coverage
68
- attribute_call = Cistern::Coverage.find_caller_before('cistern/attributes.rb')
45
+ def attribute(name, options = {})
46
+ name_sym = name.to_sym
69
47
 
70
- # Only use DSL attribute calls from within a model
71
- if attribute_call && attribute_call.label.start_with?('<class:')
72
- options[:coverage_file] = attribute_call.absolute_path
73
- options[:coverage_line] = attribute_call.lineno
74
- options[:coverage_hits] = 0
75
- end
48
+ if attributes.key?(name_sym)
49
+ fail(ArgumentError, "#{self.name} attribute[#{name_sym}] specified more than once")
76
50
  end
77
51
 
78
- name = _name.to_s.to_sym
52
+ add_coverage(options)
79
53
 
80
- send(:define_method, name) do
81
- read_attribute(name)
82
- end unless instance_methods.include?(name)
54
+ normalize_options(options)
83
55
 
84
- send(:alias_method, "#{name}?", name) if options[:type] == :boolean
85
-
86
- send(:define_method, "#{name}=") do |value|
87
- write_attribute(name, value)
88
- end unless instance_methods.include?("#{name}=".to_sym)
56
+ attributes[name_sym] = options
89
57
 
90
- if attributes[name]
91
- fail(ArgumentError, "#{self.name} attribute[#{_name}] specified more than once")
92
- else
93
- if options[:squash]
94
- options[:squash] = Array(options[:squash]).map(&:to_s)
95
- end
96
- attributes[name] = options
97
- end
98
-
99
- options[:aliases] = Array(options[:aliases] || options[:alias]).map { |a| a.to_s.to_sym }
100
-
101
- options[:aliases].each do |new_alias|
102
- aliases[new_alias] << name.to_s.to_sym
103
- end
58
+ define_attribute_reader(name_sym, options)
59
+ define_attribute_writer(name_sym, options)
104
60
  end
105
61
 
106
62
  def identity(name, options = {})
@@ -115,6 +71,52 @@ module Cistern::Attributes
115
71
  def ignored_attributes
116
72
  @ignored_attributes ||= []
117
73
  end
74
+
75
+ protected
76
+
77
+ def add_coverage(options)
78
+ return unless defined? Cistern::Coverage
79
+
80
+ attribute_call = Cistern::Coverage.find_caller_before('cistern/attributes.rb')
81
+
82
+ # Only use DSL attribute calls from within a model
83
+ if attribute_call && attribute_call.label.start_with?('<class:')
84
+ options[:coverage_file] = attribute_call.absolute_path
85
+ options[:coverage_line] = attribute_call.lineno
86
+ options[:coverage_hits] = 0
87
+ end
88
+ end
89
+
90
+ def define_attribute_reader(name, options)
91
+ send(:define_method, name) do
92
+ read_attribute(name)
93
+ end unless instance_methods.include?(name)
94
+
95
+ send(:alias_method, "#{name}?", name) if options[:type] == :boolean
96
+
97
+ options[:aliases].each { |new_alias| aliases[new_alias] << name }
98
+ end
99
+
100
+ def define_attribute_writer(name, options)
101
+ return if instance_methods.include?("#{name}=".to_sym)
102
+
103
+ send(:define_method, "#{name}=") { |value| write_attribute(name, value) }
104
+ end
105
+
106
+ private
107
+
108
+ def normalize_options(options)
109
+ options[:squash] = Array(options[:squash]).map(&:to_s) if options[:squash]
110
+ options[:aliases] = Array(options[:aliases] || options[:alias]).map { |a| a.to_sym }
111
+
112
+ transform = options.key?(:squash) ? :squash : :none
113
+ options[:transform] ||= transforms.fetch(transform)
114
+ options[:parser] ||= parsers[options[:type]] || default_parser
115
+ end
116
+
117
+ def parent_attributes
118
+ superclass && superclass.respond_to?(:attributes) && superclass.attributes.dup
119
+ end
118
120
  end
119
121
 
120
122
  module InstanceMethods
@@ -123,7 +125,8 @@ module Cistern::Attributes
123
125
  end
124
126
 
125
127
  def read_attribute(name)
126
- key = name.to_s.to_sym
128
+ key = name.to_sym
129
+
127
130
  options = self.class.attributes[key]
128
131
  default = options[:default]
129
132
 
@@ -140,19 +143,16 @@ module Cistern::Attributes
140
143
  def write_attribute(name, value)
141
144
  options = self.class.attributes[name] || {}
142
145
 
143
- transform = Cistern::Attributes.transforms[options[:squash] ? :squash : :none] ||
144
- Cistern::Attributes.default_transform
146
+ transform = options[:transform]
145
147
 
146
- parser = Cistern::Attributes.parsers[options[:type]] ||
147
- options[:parser] ||
148
- Cistern::Attributes.default_parser
148
+ parser = options[:parser]
149
149
 
150
150
  transformed = transform.call(name, value, options)
151
151
 
152
152
  new_value = parser.call(transformed, options)
153
153
  attribute = name.to_s.to_sym
154
154
 
155
- previous_value = attributes[attribute]
155
+ previous_value = read_attribute(name)
156
156
 
157
157
  attributes[attribute] = new_value
158
158
 
@@ -170,9 +170,7 @@ module Cistern::Attributes
170
170
  end
171
171
 
172
172
  def dup
173
- copy = super
174
- copy.attributes = copy.attributes.dup
175
- copy
173
+ super.tap { |m| m.attributes = attributes.dup }
176
174
  end
177
175
 
178
176
  def identity
@@ -265,7 +263,7 @@ module Cistern::Attributes
265
263
  private
266
264
 
267
265
  def missing_attributes(keys)
268
- keys.reduce({}) { |a,e| a.merge(e => send("#{e}")) }
266
+ keys.map(&:to_sym).reduce({}) { |a,e| a.merge(e => public_send("#{e}")) }
269
267
  .partition { |_,v| v.nil? }
270
268
  .map { |s| Hash[s] }
271
269
  end
@@ -281,39 +279,32 @@ module Cistern::Attributes
281
279
  def _merge_attributes(new_attributes)
282
280
  protected_methods = (Cistern::Model.instance_methods - PROTECTED_METHODS)
283
281
  ignored_attributes = self.class.ignored_attributes
284
- class_attributes = self.class.attributes
282
+ specifications = self.class.attributes
285
283
  class_aliases = self.class.aliases
286
284
 
287
- new_attributes.each do |_key, value|
288
- string_key = _key.is_a?(String) ? _key : _key.to_s
289
- symbol_key = case _key
290
- when String
291
- _key.to_sym
292
- when Symbol
293
- _key
294
- else
295
- string_key.to_sym
296
- end
285
+ # this has the side effect of dup'ing the incoming hash
286
+ new_attributes = Cistern::Hash.stringify_keys(new_attributes)
287
+
288
+ new_attributes.each do |key, value|
289
+ symbol_key = key.to_sym
297
290
 
298
291
  # find nested paths
299
- value.is_a?(::Hash) && class_attributes.each do |name, options|
300
- if options[:squash] && options[:squash].first == string_key
301
- send("#{name}=", symbol_key => value)
292
+ value.is_a?(::Hash) && specifications.each do |name, options|
293
+ if options[:squash] && options[:squash].first == key
294
+ send("#{name}=", key => value)
302
295
  end
303
296
  end
304
297
 
305
298
  next if ignored_attributes.include?(symbol_key)
306
299
 
307
300
  if class_aliases.key?(symbol_key)
308
- class_aliases[symbol_key].each do |aliased_key|
309
- send("#{aliased_key}=", value)
310
- end
301
+ class_aliases[symbol_key].each { |attribute_alias| public_send("#{attribute_alias}=", value) }
311
302
  end
312
303
 
313
- assignment_method = "#{string_key}="
304
+ assignment_method = "#{key}="
314
305
 
315
306
  if !protected_methods.include?(symbol_key) && self.respond_to?(assignment_method, true)
316
- send(assignment_method, value)
307
+ public_send(assignment_method, value)
317
308
  end
318
309
  end
319
310
  end
@@ -1,4 +1,6 @@
1
1
  module Cistern::Collection
2
+ include Cistern::HashSupport
3
+
2
4
  BLACKLISTED_ARRAY_METHODS = [
3
5
  :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
4
6
  :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
@@ -27,7 +27,7 @@ module AwesomePrint::Cistern
27
27
  # Format Cistern::Model
28
28
  #------------------------------------------------------------------------------
29
29
  def awesome_cistern_model(object)
30
- data = object.attributes.keys.inject({}) { |r, k| r.merge(k => object.send(k)) }
30
+ data = object.attributes.keys.sort.each_with_object({}) { |e, a| a[e] = object.public_send(e) }
31
31
  "#{object} " << awesome_hash(data)
32
32
  end
33
33
 
@@ -1,24 +1,38 @@
1
1
  class Cistern::Hash
2
+ # @example
3
+ # Cistern::Hash.slice({ :a => 1, :b => 2 }, :a) #=> { :a => 1 }
4
+ # @return [Hash] copy of {#hash} containing only {#keys}
2
5
  def self.slice(hash, *keys)
3
- {}.tap do |sliced|
4
- keys.each { |k| sliced[k] = hash[k] if hash.key?(k) }
5
- end
6
+ keys.each_with_object({}) { |e, a| a[e] = hash[e] if hash.key?(e) }
6
7
  end
7
8
 
9
+ # @example
10
+ # Cistern::Hash.except({ :a => 1, :b => 2 }, :a) #=> { :b => 2 }
11
+ # @return [Hash] copy of {#hash} containing all keys except {#keys}
8
12
  def self.except(hash, *keys)
9
13
  Cistern::Hash.except!(hash.dup, *keys)
10
14
  end
11
15
 
12
- # Replaces the hash without the given keys.
16
+ # Remove all keys not specified in {#keys} from {#hash} in place
17
+ #
18
+ # @example
19
+ # Cistern::Hash.except({ :a => 1, :b => 2 }, :a) #=> { :b => 2 }
20
+ # @return [Hash] {#hash}
21
+ # @see {Cistern::Hash#except}
13
22
  def self.except!(hash, *keys)
14
23
  keys.each { |key| hash.delete(key) }
15
24
  hash
16
25
  end
17
26
 
27
+ # Copy {#hash} and convert all keys to strings recursively.
28
+ #
29
+ # @example
30
+ # Cistern::Hash.stringify_keys(:a => 1, :b => 2) #=> { 'a' => 1, 'b' => 2 }
31
+ # @return [Hash] {#hash} with string keys
18
32
  def self.stringify_keys(object)
19
33
  case object
20
34
  when Hash
21
- object.inject({}) { |r, (k, v)| r.merge(k.to_s => stringify_keys(v)) }
35
+ object.each_with_object({}) { |(k, v), a| a[k.to_s] = stringify_keys(v) }
22
36
  when Array
23
37
  object.map { |v| stringify_keys(v) }
24
38
  else
@@ -0,0 +1,6 @@
1
+ module Cistern::HashSupport
2
+ def hash_slice(*args); Cistern::Hash.slice(*args); end
3
+ def hash_except(*args); Cistern::Hash.except(*args); end
4
+ def hash_except!(*args); Cistern::Hash.except!(*args); end
5
+ def hash_stringify_keys(*args); Cistern::Hash.stringify_keys(*args); end
6
+ end
@@ -1,5 +1,6 @@
1
1
  module Cistern::Model
2
2
  include Cistern::Attributes::InstanceMethods
3
+ include Cistern::HashSupport
3
4
 
4
5
  def self.included(klass)
5
6
  klass.send(:extend, Cistern::Attributes::ClassMethods)
@@ -1,4 +1,6 @@
1
1
  module Cistern::Request
2
+ include Cistern::HashSupport
3
+
2
4
  def self.cistern_request(cistern, klass, name)
3
5
  unless klass.name || klass.cistern_method
4
6
  fail ArgumentError, "can't turn anonymous class into a Cistern request"
@@ -1,44 +1,34 @@
1
1
  module Cistern::Singular
2
+ include Cistern::Model
3
+
2
4
  def self.cistern_singular(cistern, klass, name)
3
5
  cistern.const_get(:Collections).module_eval <<-EOS, __FILE__, __LINE__
4
6
  def #{name}(attributes={})
5
- #{klass.name}.new(attributes.merge(cistern: self))
7
+ #{klass.name}.new(attributes.merge(cistern: self))
6
8
  end
7
9
  EOS
8
10
  end
9
11
 
10
12
  def self.included(klass)
13
+ super
14
+
11
15
  klass.send(:extend, Cistern::Attributes::ClassMethods)
12
16
  klass.send(:include, Cistern::Attributes::InstanceMethods)
13
17
  klass.send(:extend, Cistern::Model::ClassMethods)
14
18
  end
15
19
 
16
- attr_accessor :cistern
17
-
18
- def service
19
- Cistern.deprecation(
20
- '#service is deprecated. Please use #cistern',
21
- caller[0]
22
- )
23
- @cistern
20
+ def collection
21
+ self
24
22
  end
25
23
 
26
- def inspect
27
- Cistern.formatter.call(self)
28
- end
29
-
30
- def initialize(options)
31
- merge_attributes(options)
32
- reload
24
+ def get
25
+ raise NotImplementedError
33
26
  end
34
27
 
35
28
  def reload
36
- new_attributes = fetch_attributes
37
-
38
- merge_attributes(new_attributes) if new_attributes
29
+ get
30
+ self
39
31
  end
40
32
 
41
- def fetch_attributes
42
- fail NotImplementedError
43
- end
33
+ alias load reload
44
34
  end