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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -1
- data/Appraisals +3 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile +2 -0
- data/README.md +319 -127
- data/gemfiles/ruby_lt_2.0.gemfile +24 -0
- data/lib/cistern.rb +1 -0
- data/lib/cistern/attributes.rb +101 -110
- data/lib/cistern/collection.rb +2 -0
- data/lib/cistern/formatter/awesome_print.rb +1 -1
- data/lib/cistern/hash.rb +19 -5
- data/lib/cistern/hash_support.rb +6 -0
- data/lib/cistern/model.rb +1 -0
- data/lib/cistern/request.rb +2 -0
- data/lib/cistern/singular.rb +12 -22
- data/lib/cistern/version.rb +1 -1
- data/spec/attributes_spec.rb +1 -1
- data/spec/coverage_spec.rb +1 -1
- data/spec/hash_spec.rb +35 -0
- data/spec/model_spec.rb +14 -0
- data/spec/singular_spec.rb +25 -17
- metadata +6 -2
@@ -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 => "../"
|
data/lib/cistern.rb
CHANGED
data/lib/cistern/attributes.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
}
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
54
|
-
|
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(
|
67
|
-
|
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
|
-
|
71
|
-
|
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
|
-
|
52
|
+
add_coverage(options)
|
79
53
|
|
80
|
-
|
81
|
-
read_attribute(name)
|
82
|
-
end unless instance_methods.include?(name)
|
54
|
+
normalize_options(options)
|
83
55
|
|
84
|
-
|
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
|
-
|
91
|
-
|
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.
|
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 =
|
144
|
-
Cistern::Attributes.default_transform
|
146
|
+
transform = options[:transform]
|
145
147
|
|
146
|
-
parser =
|
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 =
|
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
|
-
|
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 =>
|
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
|
-
|
282
|
+
specifications = self.class.attributes
|
285
283
|
class_aliases = self.class.aliases
|
286
284
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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) &&
|
300
|
-
if options[:squash] && options[:squash].first ==
|
301
|
-
send("#{name}=",
|
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
|
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 = "#{
|
304
|
+
assignment_method = "#{key}="
|
314
305
|
|
315
306
|
if !protected_methods.include?(symbol_key) && self.respond_to?(assignment_method, true)
|
316
|
-
|
307
|
+
public_send(assignment_method, value)
|
317
308
|
end
|
318
309
|
end
|
319
310
|
end
|
data/lib/cistern/collection.rb
CHANGED
@@ -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.
|
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
|
|
data/lib/cistern/hash.rb
CHANGED
@@ -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
|
-
{}
|
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
|
-
#
|
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.
|
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
|
data/lib/cistern/model.rb
CHANGED
data/lib/cistern/request.rb
CHANGED
data/lib/cistern/singular.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
27
|
-
|
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
|
-
|
37
|
-
|
38
|
-
merge_attributes(new_attributes) if new_attributes
|
29
|
+
get
|
30
|
+
self
|
39
31
|
end
|
40
32
|
|
41
|
-
|
42
|
-
fail NotImplementedError
|
43
|
-
end
|
33
|
+
alias load reload
|
44
34
|
end
|