jsonapi-resources 0.9.12 → 0.10.7
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/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
- data/lib/jsonapi/active_relation/join_manager.rb +303 -0
- data/lib/jsonapi/active_relation_resource.rb +884 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +122 -106
- data/lib/jsonapi/basic_resource.rb +1162 -0
- data/lib/jsonapi/cached_response_fragment.rb +127 -0
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +57 -8
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/error_codes.rb +2 -0
- data/lib/jsonapi/exceptions.rb +63 -40
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +18 -75
- data/lib/jsonapi/link_builder.rb +18 -25
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +73 -15
- data/lib/jsonapi/paginator.rb +17 -0
- data/lib/jsonapi/path.rb +43 -0
- data/lib/jsonapi/path_segment.rb +76 -0
- data/lib/jsonapi/processor.rb +246 -111
- data/lib/jsonapi/relationship.rb +117 -24
- data/lib/jsonapi/request_parser.rb +383 -396
- data/lib/jsonapi/resource.rb +3 -1376
- data/lib/jsonapi/resource_controller_metal.rb +3 -0
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +124 -286
- data/lib/jsonapi/resource_set.rb +176 -0
- data/lib/jsonapi/resources/railtie.rb +9 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +104 -87
- data/lib/jsonapi/routing_ext.rb +19 -21
- data/lib/jsonapi-resources.rb +20 -4
- data/lib/tasks/check_upgrade.rake +52 -0
- metadata +34 -31
- data/lib/jsonapi/cached_resource_fragment.rb +0 -127
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -4,14 +4,12 @@ module JSONAPI
|
|
4
4
|
# For example ['posts.comments.tags']
|
5
5
|
# will transform into =>
|
6
6
|
# {
|
7
|
-
# posts:{
|
8
|
-
#
|
9
|
-
# include_related:{
|
7
|
+
# posts: {
|
8
|
+
# include_related: {
|
10
9
|
# comments:{
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# include:true
|
10
|
+
# include_related: {
|
11
|
+
# tags: {
|
12
|
+
# include_related: {}
|
15
13
|
# }
|
16
14
|
# }
|
17
15
|
# }
|
@@ -19,9 +17,8 @@ module JSONAPI
|
|
19
17
|
# }
|
20
18
|
# }
|
21
19
|
|
22
|
-
def initialize(resource_klass, includes_array
|
20
|
+
def initialize(resource_klass, includes_array)
|
23
21
|
@resource_klass = resource_klass
|
24
|
-
@force_eager_load = force_eager_load
|
25
22
|
@include_directives_hash = { include_related: {} }
|
26
23
|
includes_array.each do |include|
|
27
24
|
parse_include(include)
|
@@ -32,79 +29,25 @@ module JSONAPI
|
|
32
29
|
@include_directives_hash
|
33
30
|
end
|
34
31
|
|
35
|
-
def model_includes
|
36
|
-
get_includes(@include_directives_hash)
|
37
|
-
end
|
38
|
-
|
39
|
-
def paths
|
40
|
-
delve_paths(get_includes(@include_directives_hash, false))
|
41
|
-
end
|
42
|
-
|
43
|
-
def merge_filter(relation, filter)
|
44
|
-
config = include_config(relation.to_sym)
|
45
|
-
config[:include_filters] ||= {}
|
46
|
-
config[:include_filters].merge!(filter)
|
47
|
-
end
|
48
|
-
|
49
|
-
def include_config(relation)
|
50
|
-
@include_directives_hash[:include_related][relation]
|
51
|
-
end
|
52
|
-
|
53
32
|
private
|
54
33
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
if current_resource_klass
|
62
|
-
current_relationship = current_resource_klass._relationships[fragment]
|
63
|
-
current_resource_klass = current_relationship.try(:resource_klass)
|
64
|
-
else
|
65
|
-
raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
|
66
|
-
end
|
67
|
-
|
68
|
-
include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
|
69
|
-
|
70
|
-
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
|
71
|
-
current = current[:include_related][fragment]
|
72
|
-
end
|
73
|
-
current
|
74
|
-
end
|
75
|
-
|
76
|
-
def get_includes(directive, only_joined_includes = true)
|
77
|
-
ir = directive[:include_related]
|
78
|
-
ir = ir.select { |k,v| v[:include_in_join] } if only_joined_includes
|
34
|
+
def parse_include(include)
|
35
|
+
path = JSONAPI::Path.new(resource_klass: @resource_klass,
|
36
|
+
path_string: include,
|
37
|
+
ensure_default_field: false,
|
38
|
+
parse_fields: false)
|
79
39
|
|
80
|
-
|
81
|
-
sub = get_includes(sub_directive, only_joined_includes)
|
82
|
-
sub.any? ? { name => sub } : name
|
83
|
-
end
|
84
|
-
end
|
40
|
+
current = @include_directives_hash
|
85
41
|
|
86
|
-
|
87
|
-
|
88
|
-
local_path = ''
|
42
|
+
path.segments.each do |segment|
|
43
|
+
relationship_name = segment.relationship.name.to_sym
|
89
44
|
|
90
|
-
|
91
|
-
|
92
|
-
related = get_related(local_path)
|
93
|
-
related[:include] = true
|
45
|
+
current[:include_related][relationship_name] ||= { include_related: {} }
|
46
|
+
current = current[:include_related][relationship_name]
|
94
47
|
end
|
95
|
-
end
|
96
48
|
|
97
|
-
|
98
|
-
|
99
|
-
when Array
|
100
|
-
obj.map{|elem| delve_paths(elem)}.flatten(1)
|
101
|
-
when Hash
|
102
|
-
obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
|
103
|
-
when Symbol, String
|
104
|
-
[[obj]]
|
105
|
-
else
|
106
|
-
raise "delve_paths cannot descend into #{obj.class.name}"
|
107
|
-
end
|
49
|
+
rescue JSONAPI::Exceptions::InvalidRelationship => _e
|
50
|
+
raise JSONAPI::Exceptions::InvalidInclude.new(@resource_klass, include)
|
108
51
|
end
|
109
52
|
end
|
110
53
|
end
|
data/lib/jsonapi/link_builder.rb
CHANGED
@@ -29,7 +29,7 @@ module JSONAPI
|
|
29
29
|
def primary_resources_url
|
30
30
|
if @primary_resource_klass._routed
|
31
31
|
primary_resources_path = resources_path(primary_resource_klass)
|
32
|
-
@primary_resources_url_cached ||= "#{ base_url }#{
|
32
|
+
@primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
|
33
33
|
else
|
34
34
|
if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
|
35
35
|
warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
|
@@ -105,19 +105,16 @@ module JSONAPI
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def formatted_module_path_from_class(klass)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
else
|
119
|
-
"/"
|
120
|
-
end
|
108
|
+
scopes = if @engine
|
109
|
+
module_scopes_from_class(klass)[1..-1]
|
110
|
+
else
|
111
|
+
module_scopes_from_class(klass)
|
112
|
+
end
|
113
|
+
|
114
|
+
unless scopes.empty?
|
115
|
+
"/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
|
116
|
+
else
|
117
|
+
"/"
|
121
118
|
end
|
122
119
|
end
|
123
120
|
|
@@ -126,24 +123,20 @@ module JSONAPI
|
|
126
123
|
end
|
127
124
|
|
128
125
|
def resources_path(source_klass)
|
129
|
-
|
126
|
+
@_resources_path ||= {}
|
127
|
+
@_resources_path[source_klass] ||= formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
|
130
128
|
end
|
131
129
|
|
132
130
|
def resource_path(source)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
131
|
+
if source.class.singleton?
|
132
|
+
resources_path(source.class)
|
133
|
+
else
|
134
|
+
"#{resources_path(source.class)}/#{source.id}"
|
137
135
|
end
|
138
|
-
url
|
139
136
|
end
|
140
137
|
|
141
138
|
def resource_url(source)
|
142
|
-
"#{ base_url }#{
|
143
|
-
end
|
144
|
-
|
145
|
-
def serialized_engine_mount_point
|
146
|
-
engine_mount_point == "/" ? "" : engine_mount_point
|
139
|
+
"#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
|
147
140
|
end
|
148
141
|
|
149
142
|
def route_for_relationship(relationship)
|
data/lib/jsonapi/operation.rb
CHANGED
@@ -8,17 +8,28 @@ module JSONAPI
|
|
8
8
|
@options = options
|
9
9
|
end
|
10
10
|
|
11
|
-
def transactional?
|
12
|
-
JSONAPI::Processor._processor_from_resource_type(resource_klass).transactional_operation_type?(operation_type)
|
13
|
-
end
|
14
|
-
|
15
11
|
def process
|
16
12
|
processor.process
|
17
13
|
end
|
18
14
|
|
19
15
|
private
|
20
16
|
def processor
|
21
|
-
|
17
|
+
self.class.processor_instance_for(resource_klass, operation_type, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def processor_instance_for(resource_klass, operation_type, params)
|
22
|
+
_processor_from_resource_type(resource_klass).new(resource_klass, operation_type, params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def _processor_from_resource_type(resource_klass)
|
26
|
+
processor = resource_klass.name.gsub(/Resource$/,'Processor').safe_constantize
|
27
|
+
if processor.nil?
|
28
|
+
processor = JSONAPI.configuration.default_processor_klass
|
29
|
+
end
|
30
|
+
|
31
|
+
return processor
|
32
|
+
end
|
22
33
|
end
|
23
34
|
end
|
24
35
|
end
|
@@ -4,12 +4,18 @@ module JSONAPI
|
|
4
4
|
attr_accessor :meta
|
5
5
|
attr_accessor :links
|
6
6
|
attr_accessor :options
|
7
|
+
attr_accessor :warnings
|
7
8
|
|
8
9
|
def initialize(code, options = {})
|
9
|
-
@code = code
|
10
|
+
@code = Rack::Utils.status_code(code)
|
10
11
|
@options = options
|
11
12
|
@meta = options.fetch(:meta, {})
|
12
13
|
@links = options.fetch(:links, {})
|
14
|
+
@warnings = options.fetch(:warnings, {})
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash(serializer = nil)
|
18
|
+
{}
|
13
19
|
end
|
14
20
|
end
|
15
21
|
|
@@ -20,46 +26,98 @@ module JSONAPI
|
|
20
26
|
@errors = errors
|
21
27
|
super(code, options)
|
22
28
|
end
|
29
|
+
|
30
|
+
def to_hash(serializer = nil)
|
31
|
+
{
|
32
|
+
errors: errors.collect do |error|
|
33
|
+
# :nocov:
|
34
|
+
error.to_hash
|
35
|
+
# :nocov:
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
23
39
|
end
|
24
40
|
|
25
|
-
class
|
26
|
-
attr_accessor :
|
41
|
+
class ResourceSetOperationResult < OperationResult
|
42
|
+
attr_accessor :resource_set, :pagination_params
|
27
43
|
|
28
|
-
def initialize(code,
|
29
|
-
@
|
44
|
+
def initialize(code, resource_set, options = {})
|
45
|
+
@resource_set = resource_set
|
46
|
+
@pagination_params = options.fetch(:pagination_params, {})
|
30
47
|
super(code, options)
|
31
48
|
end
|
49
|
+
|
50
|
+
def to_hash(serializer)
|
51
|
+
if serializer
|
52
|
+
serializer.serialize_resource_set_to_hash_single(resource_set)
|
53
|
+
else
|
54
|
+
# :nocov:
|
55
|
+
{}
|
56
|
+
# :nocov:
|
57
|
+
end
|
58
|
+
end
|
32
59
|
end
|
33
60
|
|
34
|
-
class
|
35
|
-
attr_accessor :
|
61
|
+
class ResourcesSetOperationResult < OperationResult
|
62
|
+
attr_accessor :resource_set, :pagination_params, :record_count, :page_count
|
36
63
|
|
37
|
-
def initialize(code,
|
38
|
-
@
|
64
|
+
def initialize(code, resource_set, options = {})
|
65
|
+
@resource_set = resource_set
|
39
66
|
@pagination_params = options.fetch(:pagination_params, {})
|
40
67
|
@record_count = options[:record_count]
|
41
68
|
@page_count = options[:page_count]
|
42
69
|
super(code, options)
|
43
70
|
end
|
71
|
+
|
72
|
+
def to_hash(serializer)
|
73
|
+
if serializer
|
74
|
+
serializer.serialize_resource_set_to_hash_plural(resource_set)
|
75
|
+
else
|
76
|
+
# :nocov:
|
77
|
+
{}
|
78
|
+
# :nocov:
|
79
|
+
end
|
80
|
+
end
|
44
81
|
end
|
45
82
|
|
46
|
-
class
|
47
|
-
attr_accessor :source_resource, :_type
|
83
|
+
class RelatedResourcesSetOperationResult < ResourcesSetOperationResult
|
84
|
+
attr_accessor :resource_set, :source_resource, :_type
|
48
85
|
|
49
|
-
def initialize(code, source_resource, type,
|
86
|
+
def initialize(code, source_resource, type, resource_set, options = {})
|
50
87
|
@source_resource = source_resource
|
51
88
|
@_type = type
|
52
|
-
super(code,
|
89
|
+
super(code, resource_set, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_hash(serializer = nil)
|
93
|
+
if serializer
|
94
|
+
serializer.serialize_related_resource_set_to_hash_plural(resource_set, source_resource)
|
95
|
+
else
|
96
|
+
# :nocov:
|
97
|
+
{}
|
98
|
+
# :nocov:
|
99
|
+
end
|
53
100
|
end
|
54
101
|
end
|
55
102
|
|
56
103
|
class RelationshipOperationResult < OperationResult
|
57
|
-
attr_accessor :parent_resource, :relationship
|
104
|
+
attr_accessor :parent_resource, :relationship, :resource_ids
|
58
105
|
|
59
|
-
def initialize(code, parent_resource, relationship, options = {})
|
106
|
+
def initialize(code, parent_resource, relationship, resource_ids, options = {})
|
60
107
|
@parent_resource = parent_resource
|
61
108
|
@relationship = relationship
|
109
|
+
@resource_ids = resource_ids
|
62
110
|
super(code, options)
|
63
111
|
end
|
112
|
+
|
113
|
+
def to_hash(serializer = nil)
|
114
|
+
if serializer
|
115
|
+
serializer.serialize_to_relationship_hash(parent_resource, relationship, resource_ids)
|
116
|
+
else
|
117
|
+
# :nocov:
|
118
|
+
{}
|
119
|
+
# :nocov:
|
120
|
+
end
|
121
|
+
end
|
64
122
|
end
|
65
123
|
end
|
data/lib/jsonapi/paginator.rb
CHANGED
@@ -13,9 +13,16 @@ module JSONAPI
|
|
13
13
|
# :nocov:
|
14
14
|
end
|
15
15
|
|
16
|
+
def requires_record_count
|
17
|
+
# :nocov:
|
18
|
+
self.class.requires_record_count
|
19
|
+
# :nocov:
|
20
|
+
end
|
21
|
+
|
16
22
|
class << self
|
17
23
|
def requires_record_count
|
18
24
|
# :nocov:
|
25
|
+
# @deprecated
|
19
26
|
false
|
20
27
|
# :nocov:
|
21
28
|
end
|
@@ -36,10 +43,15 @@ class OffsetPaginator < JSONAPI::Paginator
|
|
36
43
|
verify_pagination_params
|
37
44
|
end
|
38
45
|
|
46
|
+
# @deprecated
|
39
47
|
def self.requires_record_count
|
40
48
|
true
|
41
49
|
end
|
42
50
|
|
51
|
+
def requires_record_count
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
43
55
|
def apply(relation, _order_options)
|
44
56
|
relation.offset(@offset).limit(@limit)
|
45
57
|
end
|
@@ -127,10 +139,15 @@ class PagedPaginator < JSONAPI::Paginator
|
|
127
139
|
verify_pagination_params
|
128
140
|
end
|
129
141
|
|
142
|
+
# @deprecated
|
130
143
|
def self.requires_record_count
|
131
144
|
true
|
132
145
|
end
|
133
146
|
|
147
|
+
def requires_record_count
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
134
151
|
def calculate_page_count(record_count)
|
135
152
|
(record_count / @size.to_f).ceil
|
136
153
|
end
|
data/lib/jsonapi/path.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class Path
|
3
|
+
attr_reader :segments, :resource_klass
|
4
|
+
def initialize(resource_klass:,
|
5
|
+
path_string:,
|
6
|
+
ensure_default_field: true,
|
7
|
+
parse_fields: true)
|
8
|
+
@resource_klass = resource_klass
|
9
|
+
|
10
|
+
current_resource_klass = resource_klass
|
11
|
+
@segments = path_string.to_s.split('.').collect do |segment_string|
|
12
|
+
segment = PathSegment.parse(source_resource_klass: current_resource_klass,
|
13
|
+
segment_string: segment_string,
|
14
|
+
parse_fields: parse_fields)
|
15
|
+
|
16
|
+
current_resource_klass = segment.resource_klass
|
17
|
+
segment
|
18
|
+
end
|
19
|
+
|
20
|
+
if ensure_default_field && parse_fields && @segments.last.is_a?(PathSegment::Relationship)
|
21
|
+
last = @segments.last
|
22
|
+
@segments << PathSegment::Field.new(resource_klass: last.resource_klass,
|
23
|
+
field_name: last.resource_klass._primary_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def relationship_segments
|
28
|
+
@segments.select {|p| p.is_a?(PathSegment::Relationship)}
|
29
|
+
end
|
30
|
+
|
31
|
+
def relationship_path_string
|
32
|
+
relationship_segments.collect(&:to_s).join('.')
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_relationship
|
36
|
+
if @segments.last.is_a?(PathSegment::Relationship)
|
37
|
+
@segments.last
|
38
|
+
else
|
39
|
+
@segments[-2]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class PathSegment
|
3
|
+
def self.parse(source_resource_klass:, segment_string:, parse_fields: true)
|
4
|
+
first_part, last_part = segment_string.split('#', 2)
|
5
|
+
relationship = source_resource_klass._relationship(first_part)
|
6
|
+
|
7
|
+
if relationship
|
8
|
+
if last_part
|
9
|
+
unless relationship.resource_types.include?(last_part)
|
10
|
+
raise JSONAPI::Exceptions::InvalidRelationship.new(source_resource_klass._type, segment_string)
|
11
|
+
end
|
12
|
+
resource_klass = source_resource_klass.resource_klass_for(last_part)
|
13
|
+
end
|
14
|
+
return PathSegment::Relationship.new(relationship: relationship, resource_klass: resource_klass)
|
15
|
+
else
|
16
|
+
if last_part.blank? && parse_fields
|
17
|
+
return PathSegment::Field.new(resource_klass: source_resource_klass, field_name: first_part)
|
18
|
+
else
|
19
|
+
raise JSONAPI::Exceptions::InvalidRelationship.new(source_resource_klass._type, segment_string)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Relationship
|
25
|
+
attr_reader :relationship, :resource_klass
|
26
|
+
|
27
|
+
def initialize(relationship:, resource_klass: nil)
|
28
|
+
@relationship = relationship
|
29
|
+
@resource_klass = resource_klass
|
30
|
+
end
|
31
|
+
|
32
|
+
def eql?(other)
|
33
|
+
other.is_a?(JSONAPI::PathSegment::Relationship) && relationship == other.relationship && resource_klass == other.resource_klass
|
34
|
+
end
|
35
|
+
|
36
|
+
def hash
|
37
|
+
[relationship, resource_klass].hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@resource_klass ? "#{relationship.parent_resource_klass._type}.#{relationship.name}##{resource_klass._type}" : "#{resource_klass._type}.#{relationship.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_klass
|
45
|
+
@resource_klass || relationship.resource_klass
|
46
|
+
end
|
47
|
+
|
48
|
+
def path_specified_resource_klass?
|
49
|
+
!@resource_klass.nil?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Field
|
54
|
+
attr_reader :resource_klass, :field_name
|
55
|
+
|
56
|
+
def initialize(resource_klass:, field_name:)
|
57
|
+
@resource_klass = resource_klass
|
58
|
+
@field_name = field_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def eql?(other)
|
62
|
+
other.is_a?(JSONAPI::PathSegment::Field) && field_name == other.field_name && resource_klass == other.resource_klass
|
63
|
+
end
|
64
|
+
|
65
|
+
def delegated_field_name
|
66
|
+
resource_klass._attribute_delegated_name(field_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
# :nocov:
|
71
|
+
"#{resource_klass._type}.#{field_name.to_s}"
|
72
|
+
# :nocov:
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|