granite 0.7.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 +7 -0
- data/LICENSE +22 -0
- data/app/controllers/granite/controller.rb +44 -0
- data/lib/generators/USAGE +25 -0
- data/lib/generators/granite/install_controller_generator.rb +15 -0
- data/lib/generators/granite_generator.rb +32 -0
- data/lib/generators/templates/granite_action.rb.erb +22 -0
- data/lib/generators/templates/granite_action_spec.rb.erb +45 -0
- data/lib/generators/templates/granite_base_action.rb.erb +2 -0
- data/lib/generators/templates/granite_business_action.rb.erb +3 -0
- data/lib/granite.rb +24 -0
- data/lib/granite/action.rb +106 -0
- data/lib/granite/action/error.rb +14 -0
- data/lib/granite/action/performer.rb +23 -0
- data/lib/granite/action/performing.rb +132 -0
- data/lib/granite/action/policies.rb +92 -0
- data/lib/granite/action/policies/always_allow_strategy.rb +13 -0
- data/lib/granite/action/policies/any_strategy.rb +12 -0
- data/lib/granite/action/policies/required_performer_strategy.rb +14 -0
- data/lib/granite/action/preconditions.rb +107 -0
- data/lib/granite/action/preconditions/base_precondition.rb +25 -0
- data/lib/granite/action/preconditions/embedded_precondition.rb +42 -0
- data/lib/granite/action/projectors.rb +100 -0
- data/lib/granite/action/represents.rb +26 -0
- data/lib/granite/action/represents/attribute.rb +90 -0
- data/lib/granite/action/represents/reflection.rb +15 -0
- data/lib/granite/action/subject.rb +73 -0
- data/lib/granite/action/transaction.rb +40 -0
- data/lib/granite/action/translations.rb +39 -0
- data/lib/granite/action/types.rb +1 -0
- data/lib/granite/action/types/collection.rb +13 -0
- data/lib/granite/config.rb +23 -0
- data/lib/granite/context.rb +28 -0
- data/lib/granite/dispatcher.rb +64 -0
- data/lib/granite/error.rb +4 -0
- data/lib/granite/performer_proxy.rb +34 -0
- data/lib/granite/performer_proxy/proxy.rb +31 -0
- data/lib/granite/projector.rb +48 -0
- data/lib/granite/projector/controller_actions.rb +47 -0
- data/lib/granite/projector/error.rb +14 -0
- data/lib/granite/projector/helpers.rb +59 -0
- data/lib/granite/projector/translations.rb +52 -0
- data/lib/granite/projector/translations/helper.rb +22 -0
- data/lib/granite/projector/translations/view_helper.rb +12 -0
- data/lib/granite/rails.rb +11 -0
- data/lib/granite/routing.rb +4 -0
- data/lib/granite/routing/cache.rb +24 -0
- data/lib/granite/routing/caching.rb +18 -0
- data/lib/granite/routing/declarer.rb +25 -0
- data/lib/granite/routing/mapper.rb +15 -0
- data/lib/granite/routing/mapping.rb +23 -0
- data/lib/granite/routing/route.rb +29 -0
- data/lib/granite/rspec.rb +5 -0
- data/lib/granite/rspec/action_helpers.rb +8 -0
- data/lib/granite/rspec/have_projector.rb +24 -0
- data/lib/granite/rspec/projector_helpers.rb +54 -0
- data/lib/granite/rspec/raise_validation_error.rb +52 -0
- data/lib/granite/rspec/satisfy_preconditions.rb +96 -0
- metadata +338 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'granite/action/error'
|
2
|
+
require 'granite/action/policies/any_strategy'
|
3
|
+
require 'granite/action/policies/always_allow_strategy'
|
4
|
+
require 'granite/action/policies/required_performer_strategy'
|
5
|
+
|
6
|
+
module Granite
|
7
|
+
class Action
|
8
|
+
class NotAllowedError < Error
|
9
|
+
def initialize(action)
|
10
|
+
if action.performer.respond_to?(:id) && action.performer.id.present?
|
11
|
+
performer_id = "##{action.performer.id}"
|
12
|
+
end
|
13
|
+
|
14
|
+
super("#{action.class} action is not allowed " \
|
15
|
+
"for #{action.performer.class}#{performer_id}", action)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Policies module used for abilities definition. Basically
|
20
|
+
# policies are defined as blocks which are executed in action
|
21
|
+
# instance context, so performer, object and all the attributes
|
22
|
+
# are available inside the block.
|
23
|
+
#
|
24
|
+
# By default action is allowed to be performed only by default performer.
|
25
|
+
#
|
26
|
+
module Policies
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
|
29
|
+
included do
|
30
|
+
class_attribute :_policies, :_policies_strategy, instance_writer: false
|
31
|
+
self._policies = []
|
32
|
+
self._policies_strategy = AnyStrategy
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
# The simplies policy. Takes block and executes it returning
|
37
|
+
# boolean result. Multiple policies are reduced with ||
|
38
|
+
#
|
39
|
+
# class Action < Granite::Action
|
40
|
+
# allow_if { performer.is_a?(Recruiter) }
|
41
|
+
# allow_if { performer.is_a?(AdvancedRecruiter) }
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# The first argument in block is a current action performer,
|
45
|
+
# so it is possible to use a short-cut performer methods:
|
46
|
+
#
|
47
|
+
# class Action < Granite::Action
|
48
|
+
# allow_if(&:staff?)
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
def allow_if(&block)
|
52
|
+
self._policies += [block]
|
53
|
+
end
|
54
|
+
|
55
|
+
def allow_self
|
56
|
+
allow_if { performer == subject }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def try_perform!(*)
|
61
|
+
authorize!
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
def perform(*)
|
66
|
+
authorize!
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
def perform!(*)
|
71
|
+
authorize!
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns true if any of defined policies returns true
|
76
|
+
#
|
77
|
+
def allowed?
|
78
|
+
unless instance_variable_defined?(:@allowed)
|
79
|
+
@allowed = _policies_strategy.allowed?(self)
|
80
|
+
end
|
81
|
+
@allowed
|
82
|
+
end
|
83
|
+
|
84
|
+
# Raises Granite::Action::NotAllowedError if action is not allowed
|
85
|
+
#
|
86
|
+
def authorize!
|
87
|
+
fail Granite::Action::NotAllowedError, self unless allowed?
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Policies
|
4
|
+
# A Granite policies strategy which allows an action to be performed unconditionally.
|
5
|
+
# No defined policies are evaluated.
|
6
|
+
class AlwaysAllowStrategy
|
7
|
+
def self.allowed?(_action)
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Policies
|
4
|
+
# Granite BA policy which allows action to be performed if at least one defined policy evaluates to true
|
5
|
+
class AnyStrategy
|
6
|
+
def self.allowed?(action)
|
7
|
+
action._policies.any? { |policy| action.instance_exec(action.performer, &policy) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Policies
|
4
|
+
# A Granite policies strategy which requires a performer to be present
|
5
|
+
#
|
6
|
+
# and at least one defined policy to be evaluated to true
|
7
|
+
class RequiredPerformerStrategy < AnyStrategy
|
8
|
+
def self.allowed?(action)
|
9
|
+
action.performer.present? && super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'granite/action/preconditions/base_precondition'
|
2
|
+
require 'granite/action/preconditions/embedded_precondition'
|
3
|
+
|
4
|
+
module Granite
|
5
|
+
class Action
|
6
|
+
# Conditions module is used to define preconditions for actions.
|
7
|
+
# Each precondition is also defined as validation, so it always run
|
8
|
+
# before action execution. Precondition name is by default
|
9
|
+
# I18n key for +:base+ error, if precondition fails. Along with
|
10
|
+
# preconditions question methods with the same names are created.
|
11
|
+
#
|
12
|
+
module Preconditions
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
class PreconditionsCollection
|
16
|
+
def initialize(*preconditions)
|
17
|
+
@preconditions = preconditions.flatten
|
18
|
+
end
|
19
|
+
|
20
|
+
def +(other)
|
21
|
+
self.class.new(*@preconditions, other)
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute!(context)
|
25
|
+
@preconditions.each { |precondition| precondition.execute!(context) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
included do
|
30
|
+
class_attribute :_preconditions, instance_writer: false
|
31
|
+
self._preconditions = PreconditionsCollection.new
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
# Define preconditions for current action.
|
36
|
+
#
|
37
|
+
# @param options [Hash] hash with
|
38
|
+
# @option message [String, Symbol] error message
|
39
|
+
# @option group [Symbol] procondition group(s)
|
40
|
+
# @param block [Block] which returns truthy value when precondition
|
41
|
+
# should pass.
|
42
|
+
def precondition(*args, &block)
|
43
|
+
options = args.extract_options!
|
44
|
+
if block
|
45
|
+
add_precondition(BasePrecondition, options, &block)
|
46
|
+
else
|
47
|
+
common_options = options.extract!(:if, :unless)
|
48
|
+
args.each do |type|
|
49
|
+
precondition common_options.merge(type => {})
|
50
|
+
end
|
51
|
+
options.each do |key, value|
|
52
|
+
value = Array.wrap(value)
|
53
|
+
precondition_options = value.extract_options!
|
54
|
+
add_precondition(klass(key), *value, precondition_options.merge!(common_options))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def klass(key)
|
62
|
+
key = key.to_s.camelize
|
63
|
+
Granite.precondition_namespaces.reduce(nil) do |memo, ns|
|
64
|
+
memo || "#{ns.to_s.camelize}::#{key}Precondition".safe_constantize
|
65
|
+
end || fail(NameError, "No precondition class for #{key}Precondition")
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_precondition(klass, *args, &block)
|
69
|
+
self._preconditions += klass.new(*args, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_reader :failed_preconditions
|
74
|
+
|
75
|
+
def initialize(*)
|
76
|
+
@failed_preconditions = []
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if all preconditions are satisfied
|
81
|
+
#
|
82
|
+
# @return [Boolean] wheter all preconditions are satisfied
|
83
|
+
def satisfy_preconditions?
|
84
|
+
errors.clear
|
85
|
+
failed_preconditions.clear
|
86
|
+
run_preconditions!
|
87
|
+
end
|
88
|
+
|
89
|
+
# Adds passed error message and options to `errors` object
|
90
|
+
def decline_with(*args)
|
91
|
+
errors.add(:base, *args)
|
92
|
+
failed_preconditions << args.first
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def run_preconditions!
|
98
|
+
_preconditions.execute! self
|
99
|
+
errors.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
def run_validations!
|
103
|
+
run_preconditions! && super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Preconditions
|
4
|
+
class BasePrecondition
|
5
|
+
def initialize(*args, &block)
|
6
|
+
@options = args.extract_options!
|
7
|
+
@args = args
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute!(context)
|
12
|
+
return if @options[:if] && !context.instance_exec(&@options[:if])
|
13
|
+
return if @options[:unless] && context.instance_exec(&@options[:unless])
|
14
|
+
_execute(context)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def _execute(context)
|
20
|
+
context.instance_exec(&@block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'granite/action/preconditions/base_precondition'
|
2
|
+
|
3
|
+
module Granite
|
4
|
+
class Action
|
5
|
+
module Preconditions
|
6
|
+
# Checks related business actions for precondition errors and adds them to current action.
|
7
|
+
#
|
8
|
+
# memoize def child_action
|
9
|
+
# ...
|
10
|
+
# end
|
11
|
+
# precondition embedded: :child_action
|
12
|
+
#
|
13
|
+
# memoize def child_action
|
14
|
+
# ...
|
15
|
+
# end
|
16
|
+
# memoize def child_actions
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
# precondition embedded: [:child_action, :child_actions]
|
20
|
+
#
|
21
|
+
class EmbeddedPrecondition < BasePrecondition
|
22
|
+
private
|
23
|
+
|
24
|
+
def _execute(context)
|
25
|
+
associations = Array.wrap(@args.first)
|
26
|
+
associations.each do |name|
|
27
|
+
actions = Array.wrap(context.__send__(name))
|
28
|
+
actions.each do |action|
|
29
|
+
decline_action(context, action)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def decline_action(context, action)
|
35
|
+
return if action.satisfy_preconditions?
|
36
|
+
action.errors[:base].each { |error| context.errors.add(:base, error) }
|
37
|
+
action.failed_preconditions.each { |error| context.failed_preconditions << error }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Projectors
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class ProjectorsCollection
|
7
|
+
def initialize(action_class)
|
8
|
+
@action_class = action_class
|
9
|
+
@storage = {}
|
10
|
+
@cache = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(name)
|
14
|
+
@cache[name.to_sym] ||= setup_projector(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def store(name, options, &block)
|
18
|
+
old_options, old_blocks = fetch_options_and_blocks(name)
|
19
|
+
@storage[name.to_sym] = [
|
20
|
+
old_options.merge(options || {}),
|
21
|
+
old_blocks + [block].compact
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
def names
|
26
|
+
@storage.keys | (@action_class.superclass < Granite::Action ? @action_class.superclass._projectors.names : [])
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def setup_projector(name)
|
32
|
+
options, blocks = fetch_options_and_blocks(name)
|
33
|
+
|
34
|
+
projector_name = "#{name}_projector".classify
|
35
|
+
controller_name = "#{name}_controller".classify
|
36
|
+
|
37
|
+
projector = Class.new(projector_superclass(name, projector_name, options))
|
38
|
+
projector.action_class = @action_class
|
39
|
+
|
40
|
+
redefine_const(projector_name, projector)
|
41
|
+
redefine_const(controller_name, projector.controller_class)
|
42
|
+
|
43
|
+
blocks.each do |block|
|
44
|
+
projector.class_eval(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
projector
|
48
|
+
end
|
49
|
+
|
50
|
+
def redefine_const(name, klass)
|
51
|
+
if @action_class.const_defined?(name, false)
|
52
|
+
@action_class.__send__(:remove_const, name)
|
53
|
+
# TODO: this remove is confusing, would be better to raise? - ask @pyromaniac
|
54
|
+
end
|
55
|
+
@action_class.const_set(name, klass)
|
56
|
+
end
|
57
|
+
|
58
|
+
def fetch_options_and_blocks(name)
|
59
|
+
name = name.to_sym
|
60
|
+
options, blocks = @storage[name.to_sym]
|
61
|
+
options ||= {}
|
62
|
+
blocks ||= []
|
63
|
+
|
64
|
+
[options, blocks]
|
65
|
+
end
|
66
|
+
|
67
|
+
def projector_superclass(name, projector_name, options)
|
68
|
+
superclass = options[:class_name].presence.try(:constantize)
|
69
|
+
superclass ||= @action_class.superclass._projectors.fetch(name) if @action_class.superclass < Granite::Action
|
70
|
+
|
71
|
+
superclass || projector_name.safe_constantize || Granite::Projector
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
def _projectors
|
77
|
+
@_projectors ||= ProjectorsCollection.new(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
def projector_names
|
81
|
+
_projectors.names
|
82
|
+
end
|
83
|
+
|
84
|
+
def projector(name, options = {}, &block)
|
85
|
+
_projectors.store(name, options, &block)
|
86
|
+
|
87
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
88
|
+
def self.#{name}
|
89
|
+
_projectors.fetch(:#{name})
|
90
|
+
end
|
91
|
+
|
92
|
+
def #{name}
|
93
|
+
@#{name} ||= self.class._projectors.fetch(:#{name}).new(self)
|
94
|
+
end
|
95
|
+
METHOD
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'granite/action/represents/reflection'
|
2
|
+
|
3
|
+
module Granite
|
4
|
+
class Action
|
5
|
+
module Represents
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
private
|
10
|
+
|
11
|
+
def represents(*fields, &block)
|
12
|
+
options = fields.extract_options!.symbolize_keys
|
13
|
+
|
14
|
+
fields.each do |field|
|
15
|
+
add_attribute Granite::Action::Represents::Reflection, field, options, &block
|
16
|
+
|
17
|
+
before_validation do
|
18
|
+
attribute(field).sync if __send__ "#{field}_changed?"
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Granite
|
2
|
+
class Action
|
3
|
+
module Represents
|
4
|
+
class Attribute < ActiveData::Model::Attributes::Attribute
|
5
|
+
delegate :writer, :reader, :reader_before_type_cast, to: :reflection
|
6
|
+
|
7
|
+
def initialize(*_args)
|
8
|
+
super
|
9
|
+
|
10
|
+
set_default_value
|
11
|
+
set_default_value_before_type_cast
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync
|
15
|
+
reference.public_send(writer, read)
|
16
|
+
end
|
17
|
+
|
18
|
+
def typecast(value)
|
19
|
+
return value if value.class == type
|
20
|
+
|
21
|
+
typecaster.call(value, self) unless value.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def type
|
25
|
+
return reflection.options[:type] if reflection.options[:type].present?
|
26
|
+
active_data_type || type_from_type_for_attribute || super
|
27
|
+
end
|
28
|
+
|
29
|
+
def typecaster
|
30
|
+
@typecaster ||= begin
|
31
|
+
type_class = type.instance_of?(Class) ? type : type.class
|
32
|
+
@typecaster = ActiveData.typecaster(type_class.ancestors.grep(Class))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def reference
|
39
|
+
owner.__send__(reflection.reference)
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_default_value
|
43
|
+
return unless reference.respond_to?(reader)
|
44
|
+
|
45
|
+
variable_cache(:value) do
|
46
|
+
normalize(enumerize(reference.public_send(reader)))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_default_value_before_type_cast
|
51
|
+
return unless reference.respond_to?(reader_before_type_cast)
|
52
|
+
|
53
|
+
variable_cache(:value_before_type_cast) do
|
54
|
+
defaultize(reference.public_send(reader_before_type_cast))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def active_data_type
|
59
|
+
return nil unless reference.is_a?(ActiveData::Model)
|
60
|
+
|
61
|
+
reference_attribute = reference.attribute(name)
|
62
|
+
|
63
|
+
return Granite::Action::Types::Collection.new(reference_attribute.type) if [
|
64
|
+
ActiveData::Model::Attributes::ReferenceMany,
|
65
|
+
ActiveData::Model::Attributes::Collection,
|
66
|
+
ActiveData::Model::Attributes::Dictionary
|
67
|
+
].any? { |klass| reference_attribute.is_a? klass }
|
68
|
+
|
69
|
+
reference_attribute.type # TODO: create `type_for_attribute` method inside of ActiveData
|
70
|
+
end
|
71
|
+
|
72
|
+
def type_from_type_for_attribute
|
73
|
+
return nil unless reference.respond_to?(:type_for_attribute)
|
74
|
+
|
75
|
+
attribute_type = reference.type_for_attribute(name.to_s)
|
76
|
+
|
77
|
+
return Granite::Action::Types::Collection.new(convert_type_to_value_class(attribute_type.subtype)) if attribute_type.respond_to?(:subtype)
|
78
|
+
|
79
|
+
convert_type_to_value_class(attribute_type)
|
80
|
+
end
|
81
|
+
|
82
|
+
def convert_type_to_value_class(attribute_type)
|
83
|
+
return attribute_type.value_class if attribute_type.respond_to?(:value_class)
|
84
|
+
|
85
|
+
ActiveData::Model::Associations::PersistenceAdapters::ActiveRecord::TYPES[attribute_type.type&.to_sym]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|