platanus 0.0.32 → 0.0.49
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +0 -2
- data/lib/platanus/activable.rb +2 -0
- data/lib/platanus/canned2.rb +10 -64
- data/lib/platanus/stacked2.rb +169 -81
- data/lib/platanus/version.rb +1 -1
- data/platanus.gemspec +2 -1
- data/spec/canned2_spec.rb +3 -9
- metadata +18 -3
data/Gemfile
CHANGED
data/lib/platanus/activable.rb
CHANGED
@@ -47,6 +47,8 @@ module Platanus
|
|
47
47
|
self.transaction do
|
48
48
|
run_callbacks :remove do
|
49
49
|
|
50
|
+
# TODO: disable update callbacks and validations!
|
51
|
+
|
50
52
|
# Retrieve dependant properties and remove them.
|
51
53
|
self.class.reflect_on_all_associations.select do |assoc|
|
52
54
|
if assoc.options[:dependent] == :destroy
|
data/lib/platanus/canned2.rb
CHANGED
@@ -94,78 +94,19 @@ module Platanus
|
|
94
94
|
# TODO: example
|
95
95
|
class Profile
|
96
96
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
rule_ctx.instance_eval(&rule)
|
101
|
-
rule_ctx.passed?
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def is_forbidden()
|
106
|
-
# TODO: local forbids
|
107
|
-
end
|
108
|
-
|
109
|
-
def is_allowed()
|
110
|
-
end
|
111
|
-
|
112
|
-
def
|
113
|
-
|
114
|
-
def allowance_for(_ctx, _action, _tests)
|
115
|
-
|
116
|
-
# all shared rules must pass
|
117
|
-
return :not_applicable unless @shared.all? do |rule|
|
118
|
-
rule_ctx = RuleContext.new _ctx, _tests, @def_matcher, @def_resource
|
119
|
-
rule_ctx.instance_eval(&rule)
|
120
|
-
rule_ctx.passed?
|
121
|
-
end
|
122
|
-
|
123
|
-
# TODO: local forbids
|
124
|
-
|
125
|
-
# process base profile
|
126
|
-
unless @base.nil?
|
127
|
-
result = @base.process _ctx, _action, _tests
|
128
|
-
return result if result == :not_applicable or result == :forbidden
|
129
|
-
matches << resu
|
130
|
-
end
|
131
|
-
|
132
|
-
# process subprofiles
|
133
|
-
if @isolated.each do |profile|
|
134
|
-
result = profile.process _ctx, _action, _tests
|
135
|
-
return :forbidden if result == :forbidden
|
136
|
-
result = :allowed if result == :allowed
|
137
|
-
end
|
138
|
-
|
139
|
-
# see if any of the registered rules pass
|
140
|
-
return :allowed if @rules[_action].any? do |rule|
|
141
|
-
rule_ctx = RuleContext.new _ctx, _tests, @def_matcher, @def_resource
|
142
|
-
rule_ctx.instance_eval(&rule)
|
143
|
-
rule_ctx.passed?
|
144
|
-
end
|
145
|
-
|
146
|
-
return :not_allowed
|
147
|
-
end
|
97
|
+
attr_reader :rules
|
98
|
+
attr_reader :def_matcher
|
99
|
+
attr_reader :def_resource
|
148
100
|
|
149
101
|
# The initializer takes another profile as rules base.
|
150
|
-
def initialize(_def_matcher, _def_resource
|
151
|
-
@base = _base
|
102
|
+
def initialize(_base, _def_matcher, _def_resource)
|
152
103
|
@rules = Hash.new { |h, k| h[k] = [] }
|
153
|
-
@
|
154
|
-
@shared = []
|
155
|
-
|
104
|
+
_base.rules.each { |k, tests| @rules[k] = tests.clone } unless _base.nil?
|
156
105
|
raise Error.new 'Must provide a default test' if _def_matcher.nil?
|
157
106
|
@def_matcher = _def_matcher
|
158
107
|
@def_resource = _def_resource
|
159
108
|
end
|
160
109
|
|
161
|
-
def always(_upon=nil, &_block)
|
162
|
-
@shared << (_upon || _block)
|
163
|
-
end
|
164
|
-
|
165
|
-
def isolate(_options={}, &_block)
|
166
|
-
@isolated << Profile.new _options.fetch(, @def_matcher), _options.fetch(, @def_resource)
|
167
|
-
end
|
168
|
-
|
169
110
|
## Adds an "allowance" rule
|
170
111
|
def allow(_action, _upon=nil, &_block)
|
171
112
|
@rules[_action] << (_upon || _block)
|
@@ -176,6 +117,11 @@ module Platanus
|
|
176
117
|
# TODO
|
177
118
|
end
|
178
119
|
|
120
|
+
## Clear all rules related to an action
|
121
|
+
def clear(_action)
|
122
|
+
@rules[_action] = []
|
123
|
+
end
|
124
|
+
|
179
125
|
## SHORT HAND METHODS
|
180
126
|
|
181
127
|
def upon(_expr=nil, &_block)
|
data/lib/platanus/stacked2.rb
CHANGED
@@ -6,7 +6,7 @@ module Platanus
|
|
6
6
|
|
7
7
|
## Adds the has_stacked association to an ActiveRecord model.
|
8
8
|
#
|
9
|
-
# TODO
|
9
|
+
# TODO: Investigate how to turn this into an authentic association.
|
10
10
|
#
|
11
11
|
module StackedAttr2
|
12
12
|
|
@@ -28,49 +28,56 @@ module Platanus
|
|
28
28
|
# prepare names
|
29
29
|
tname = _name.to_s
|
30
30
|
tname_single = tname.singularize
|
31
|
-
tname_class = _options.fetch
|
31
|
+
tname_class = _options.fetch :class_name, tname_single.camelize
|
32
|
+
stacked_model = tname_class.constantize
|
33
|
+
prefix = if _options[:cache_prf].nil? then 'last_' else _options.delete(:cache_prf) end # TODO: deprecate?
|
32
34
|
|
33
|
-
#
|
35
|
+
# Generate top_value property
|
36
|
+
#
|
37
|
+
# How this property is generated can vary depending on given parameters or table structure:
|
38
|
+
# * If a top_value_key is provided in options, then a belongs_to association is created using it as foreign key.
|
39
|
+
# * If a top_xxx_id column is present, then a belongs_to association is created using if as foreign key.
|
40
|
+
# * If no key is provided, then a shorcut method that retrieves the stack's top is generated
|
41
|
+
#
|
34
42
|
top_value_prop = "top_#{tname_single}"
|
35
|
-
if _options.has_key? :top_value_key
|
36
|
-
belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options
|
43
|
+
top_value_key = if _options.has_key? :top_value_key
|
44
|
+
belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options[:top_value_key], autosave: true
|
45
|
+
_options.delete(:top_value_key)
|
37
46
|
elsif self.column_names.include? "#{top_value_prop}_id"
|
38
|
-
belongs_to top_value_prop.to_sym, class_name: tname_class
|
47
|
+
belongs_to top_value_prop.to_sym, class_name: tname_class, autosave: true
|
48
|
+
"#{top_value_prop}_id"
|
39
49
|
else
|
40
|
-
|
50
|
+
top_value_var = "@_stacked_#{tname}_top".to_sym
|
41
51
|
send :define_method, top_value_prop do
|
42
52
|
# Storing the last stacked value will not prevent race conditions
|
43
53
|
# when simultaneous updates occur.
|
44
|
-
last = instance_variable_get
|
45
|
-
return last unless last.nil?
|
46
|
-
instance_variable_set(
|
47
|
-
end
|
48
|
-
send :define_method, "#{top_value_prop}=" do |_top|
|
49
|
-
instance_variable_set(instance_var, _top)
|
54
|
+
last = instance_variable_get top_value_var
|
55
|
+
return last unless last.nil? or !last.persisted?
|
56
|
+
instance_variable_set(top_value_var, self.send(tname).all.first)
|
50
57
|
end
|
58
|
+
nil
|
51
59
|
end
|
52
|
-
send :private, "#{top_value_prop}="
|
53
60
|
|
54
|
-
|
61
|
+
# When called inside callbacks, returns the new value being put at top of the stack.
|
62
|
+
new_value_var = "@_stacked_#{tname}_new"
|
63
|
+
send :define_method, "#{tname_single}_will" do
|
64
|
+
instance_variable_get(new_value_var)
|
65
|
+
end
|
55
66
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
send :define_method, "#{attr_name}" do
|
62
|
-
return changes[attr_name] if changes.has_key? attr_name
|
63
|
-
return self.send(prefix + attr_name) if self.respond_to? prefix + attr_name # Return cached value if avaliable
|
64
|
-
top = self.send top_value_prop
|
65
|
-
return nil if top.nil?
|
66
|
-
return top.send attr_name
|
67
|
-
end
|
68
|
-
attr_accessible attr_name
|
67
|
+
# When called inside callbacks, will return the top value unless a new value is
|
68
|
+
# being pushed, in that case it returns the new value
|
69
|
+
last_value_var = "@_stacked_#{tname}_last"
|
70
|
+
send :define_method, "#{tname_single}_is" do
|
71
|
+
instance_variable_get(last_value_var)
|
69
72
|
end
|
70
73
|
|
71
|
-
#
|
74
|
+
# Prepare cached attributes
|
75
|
+
#
|
76
|
+
# Attribute caching allows the parent model to store the top value for
|
77
|
+
# some of the stacked model attributes (defined in options using the cached key)
|
78
|
+
#
|
72
79
|
to_cache = _options.delete(:cached)
|
73
|
-
|
80
|
+
if to_cache
|
74
81
|
to_cache = to_cache.map do |cache_attr|
|
75
82
|
unless cache_attr.is_a? Hash
|
76
83
|
name = cache_attr.to_s
|
@@ -83,61 +90,137 @@ module Platanus
|
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
86
|
-
# callbacks
|
87
|
-
|
93
|
+
# register callbacks
|
94
|
+
define_callbacks "stack_#{tname_single}"
|
88
95
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
96
|
+
# push logic
|
97
|
+
__update_stack = ->(_ctx, _top, _new_top, _save_quiet, &_block) do
|
98
|
+
begin
|
99
|
+
# make xx_top_value avaliable for event handlers
|
100
|
+
_ctx.instance_variable_set(new_value_var, _top) if _new_top
|
101
|
+
_ctx.instance_variable_set(last_value_var, _top)
|
93
102
|
|
94
|
-
|
95
|
-
|
103
|
+
_ctx.run_callbacks "stack_#{tname_single}" do
|
104
|
+
|
105
|
+
# cache required fields
|
106
|
+
# TODO: improve cache: convention over configuration!
|
107
|
+
# cache should be automatic given certain column names and should include aliased attribues and virtual attributes.
|
108
|
+
# has_stacked :things, cache: { prefix: '', aliases: { xx => xx }, exclude: [], virtual: { xx => xx } }
|
109
|
+
if to_cache
|
110
|
+
to_cache.each do |cache_attr|
|
111
|
+
value = if cache_attr.has_key? :from
|
112
|
+
_top.nil? ? nil : _top.send(cache_attr[:from])
|
113
|
+
else
|
114
|
+
_ctx.send(cache_attr[:virtual])
|
115
|
+
end
|
116
|
+
_ctx.send(cache_attr[:to].to_s + '=', value)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
_block.call if _block
|
121
|
+
|
122
|
+
if _new_top
|
123
|
+
# TODO: this leaves the invalid record on top of the stack and invalid cached values,
|
124
|
+
# maybe validation should ocurr before caching...
|
125
|
+
raise ActiveRecord::RecordInvalid.new(_top) unless _ctx.send(tname) << _top
|
126
|
+
end
|
127
|
+
|
128
|
+
# reset top_value_prop to top
|
129
|
+
if top_value_key
|
130
|
+
if _save_quiet
|
131
|
+
top_id = if _top.nil? then nil else _top.id end
|
132
|
+
if _ctx.send(top_value_key) != top_id
|
133
|
+
_ctx.update_column(top_value_key, top_id)
|
134
|
+
_ctx.send(top_value_prop, false) # reset belongs_to cache
|
135
|
+
end
|
136
|
+
else
|
137
|
+
_ctx.send("#{top_value_prop}=", _top)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
_ctx.instance_variable_set(top_value_var, _top)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
ensure
|
144
|
+
_ctx.instance_variable_set(new_value_var, nil)
|
145
|
+
_ctx.instance_variable_set(last_value_var, nil)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Attribute mirroring
|
150
|
+
#
|
151
|
+
# Mirroring allows using the top value attributes in the parent model,
|
152
|
+
# it also allows modifying the attributes in the parent model, if the model is
|
153
|
+
# then saved, the modified attributes are wrapped in a new stack model object and put
|
154
|
+
# on top.
|
155
|
+
#
|
156
|
+
mirror_cache_var = "@_stacked_#{tname}_mirror".to_sym
|
157
|
+
if _options.delete(:mirroring)
|
158
|
+
stacked_model.accessible_attributes.each do |attr_name|
|
159
|
+
|
160
|
+
unless self.method_defined? "#{attr_name}="
|
161
|
+
send :define_method, "#{attr_name}=" do |value|
|
162
|
+
mirror = instance_variable_get(mirror_cache_var)
|
163
|
+
mirror = instance_variable_set(mirror_cache_var, {}) if mirror.nil?
|
164
|
+
mirror[attr_name] = value
|
165
|
+
end
|
166
|
+
else
|
167
|
+
Rails.logger.warn "stacked: failed to mirror setter for #{attr_name} in #{self.to_s}"
|
168
|
+
end
|
169
|
+
|
170
|
+
unless self.method_defined? attr_name
|
171
|
+
send :define_method, attr_name do
|
172
|
+
mirror = instance_variable_get(mirror_cache_var)
|
173
|
+
return mirror[attr_name] if !mirror.nil? and mirror.has_key? attr_name
|
174
|
+
|
175
|
+
return self.send(prefix + attr_name) if self.respond_to? prefix + attr_name # return cached value if avaliable
|
176
|
+
top = self.send top_value_prop
|
177
|
+
return nil if top.nil?
|
178
|
+
return top.send attr_name
|
179
|
+
end
|
96
180
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
181
|
+
send :define_method, "#{attr_name}_changed?" do
|
182
|
+
mirror = instance_variable_get(mirror_cache_var)
|
183
|
+
return true if !mirror.nil? and mirror.has_key? attr_name
|
184
|
+
return self.send(prefix + attr_name + '_changed?') if self.respond_to? prefix + attr_name + '_changed?' # return cached value if avaliable
|
185
|
+
return true # for now just return true for non cached attributes
|
186
|
+
end
|
187
|
+
|
188
|
+
attr_accessible attr_name
|
103
189
|
else
|
104
|
-
|
190
|
+
Rails.logger.warn "stacked: failed to mirror getter for #{attr_name} in #{self.to_s}"
|
105
191
|
end
|
106
|
-
_ctx.send(cache_attr[:to].to_s + '=', value)
|
107
192
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
193
|
+
|
194
|
+
# before saving model, load changes from virtual attributes.
|
195
|
+
set_callback :save, :around do |&_block|
|
196
|
+
|
197
|
+
mirror = instance_variable_get(mirror_cache_var)
|
198
|
+
if !mirror.nil? and mirror.count > 0
|
199
|
+
|
200
|
+
# propagate non cached attributes (only if record is not new and there is a top state)
|
201
|
+
unless self.new_record?
|
202
|
+
top = self.send top_value_prop
|
203
|
+
unless top.nil?
|
204
|
+
stacked_model.accessible_attributes.each do |attr_name|
|
205
|
+
mirror[attr_name] = top.send(attr_name) unless mirror.has_key? attr_name
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
obj = stacked_model.new(mirror)
|
211
|
+
instance_variable_set(mirror_cache_var, {}) # reset mirror changes
|
212
|
+
__update_stack.call(self, obj, true, true, &_block)
|
213
|
+
|
214
|
+
else _block.call end
|
128
215
|
end
|
129
216
|
end
|
130
217
|
|
218
|
+
# Push methods
|
219
|
+
|
131
220
|
send :define_method, "push_#{tname_single}!" do |obj|
|
132
221
|
self.class.transaction do
|
133
|
-
|
134
|
-
|
135
|
-
cache_step.call(self, obj, true)
|
136
|
-
self.save! if self.new_record? # make sure there is an id BEFORE pushing
|
137
|
-
raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<', obj)
|
138
|
-
after_step.call(self, obj)
|
139
|
-
|
140
|
-
self.save! if self.changed? # Must save again, no other way...
|
222
|
+
__update_stack.call(self, obj, true, false) { self.save! if self.new_record? }
|
223
|
+
self.save! if self.changed?
|
141
224
|
end
|
142
225
|
end
|
143
226
|
|
@@ -149,15 +232,13 @@ module Platanus
|
|
149
232
|
end
|
150
233
|
end
|
151
234
|
|
235
|
+
# Restore methods
|
236
|
+
|
152
237
|
send :define_method, "restore_#{tname}!" do
|
153
238
|
self.class.transaction do
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
cache_step.call(self, top, false)
|
158
|
-
after_step.call(self, top)
|
159
|
-
|
160
|
-
self.save! if self.changed?
|
239
|
+
top = self.send(tname).all.first
|
240
|
+
__update_stack.call(self, top, false, false)
|
241
|
+
self.save! if self.changed?
|
161
242
|
end
|
162
243
|
end
|
163
244
|
|
@@ -168,6 +249,13 @@ module Platanus
|
|
168
249
|
return false
|
169
250
|
end
|
170
251
|
end
|
252
|
+
|
253
|
+
# setup main association
|
254
|
+
# TODO: Support other kind of ordering, this would require to reevaluate top on every push
|
255
|
+
_options[:order] = 'created_at DESC, id DESC'
|
256
|
+
_options[:limit] = 1 if _options[:limit].nil?
|
257
|
+
_options.delete(:limit) if _options[:limit] == :no_limit
|
258
|
+
has_many _name, _options
|
171
259
|
end
|
172
260
|
end
|
173
261
|
end
|
data/lib/platanus/version.rb
CHANGED
data/platanus.gemspec
CHANGED
@@ -12,8 +12,9 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
13
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
14
|
gem.name = "platanus"
|
15
|
-
gem.require_paths = ["lib"
|
15
|
+
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Platanus::VERSION
|
17
17
|
|
18
18
|
gem.add_runtime_dependency "multi_json", [">= 1.3.2"]
|
19
|
+
gem.add_development_dependency "rspec"
|
19
20
|
end
|
data/spec/canned2_spec.rb
CHANGED
@@ -17,12 +17,10 @@ describe Platanus::Canned2 do
|
|
17
17
|
class DummyCtx
|
18
18
|
|
19
19
|
attr_reader :params
|
20
|
-
attr_reader :action_name
|
21
20
|
attr_reader :current_user
|
22
21
|
|
23
|
-
def initialize(_user,
|
22
|
+
def initialize(_user, _params={})
|
24
23
|
@current_user = _user
|
25
|
-
@action_name = _action_name
|
26
24
|
@params = _params
|
27
25
|
end
|
28
26
|
end
|
@@ -42,7 +40,6 @@ describe Platanus::Canned2 do
|
|
42
40
|
allow 'rute1#action3', upon { same(:char1, key: "current_user.char1") }
|
43
41
|
allow 'rute1#action4', upon(:current_user) { same(:param2, key: "char2") and checks(:test1) }
|
44
42
|
allow 'rute1#action5', upon(:current_user) { passes { current_user.char2 == params[:param2] } }
|
45
|
-
allow 'rute2', upon(:current_user) { same(:char1) and action_is_not(:create) }
|
46
43
|
|
47
44
|
# Complex routes
|
48
45
|
allow 'rute1#action5' do
|
@@ -52,8 +49,8 @@ describe Platanus::Canned2 do
|
|
52
49
|
end
|
53
50
|
end
|
54
51
|
|
55
|
-
let(:good_ctx) { DummyCtx.new(DummyUsr.new(10, "200"),
|
56
|
-
let(:bad_ctx) { DummyCtx.new(DummyUsr.new(10, 30),
|
52
|
+
let(:good_ctx) { DummyCtx.new(DummyUsr.new(10, "200"), char1: '10', param2: '200') }
|
53
|
+
let(:bad_ctx) { DummyCtx.new(DummyUsr.new(10, 30), char1: '10', param2: '200') }
|
57
54
|
|
58
55
|
describe "._run" do
|
59
56
|
context 'when using single context rules' do
|
@@ -76,9 +73,6 @@ describe Platanus::Canned2 do
|
|
76
73
|
it "does authorize on rute with context and inline test" do
|
77
74
|
Roles.can?(good_ctx, :user, 'rute1#action5').should be_true
|
78
75
|
end
|
79
|
-
it "does not authorize on rute with context, match and action_is_not" do
|
80
|
-
Roles.can?(good_ctx, :user, 'rute2').should be_false
|
81
|
-
end
|
82
76
|
end
|
83
77
|
|
84
78
|
context 'when using multiple context rules' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: platanus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.49
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -27,6 +27,22 @@ dependencies:
|
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: 1.3.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
30
46
|
description: Platan.us utility gem
|
31
47
|
email:
|
32
48
|
- ignacio@platan.us
|
@@ -68,7 +84,6 @@ post_install_message:
|
|
68
84
|
rdoc_options: []
|
69
85
|
require_paths:
|
70
86
|
- lib
|
71
|
-
- lib/platanus
|
72
87
|
required_ruby_version: !ruby/object:Gem::Requirement
|
73
88
|
none: false
|
74
89
|
requirements:
|