alba 3.10.0 → 3.11.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/CHANGELOG.md +21 -0
- data/README.md +33 -10
- data/lib/alba/association.rb +1 -9
- data/lib/alba/nested_attribute.rb +7 -5
- data/lib/alba/railtie.rb +4 -4
- data/lib/alba/resource.rb +8 -8
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +33 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18b9be1d51ec5b7bed2d31fc59904a57c8dc7f4e24eb1a7cb6364a42ec8a27ec
|
|
4
|
+
data.tar.gz: 4112d0ace0fda10a9e6aee754e2b056b6488924fddbd5f12040652a2fcbbda75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bee8581bad84469254b1282b3c81bdaf493be4f2c5ee1b31747b16f2908f4e2639d783ac9fa7e6f345b361117512f0ce4b208a81a2f000e05455d3bc875b08b
|
|
7
|
+
data.tar.gz: 6939052cd6b8ea09f23df209874c858aadc032193a0ab5729bd04d33bbf7d24e3223cc642494a0095ca4a8f7a4b182fdd4cbd4e9781c1d4689e205953c2ca2e7
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## 3.11.0 2026-07-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Rails: `serialize` and `render_serialized_json` now takes `params` [#509](https://github.com/okuramasafumi/alba/pull/509)
|
|
14
|
+
- Add `Alba.non_collection_types` for declaring Enumerable classes that should not be treated as collections [#532](https://github.com/okuramasafumi/alba/pull/532)
|
|
15
|
+
- Add default superclass configuration [#547](https://github.com/okuramasafumi/alba/pull/547)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Trait redefining base-level attribute causes wrong result with multiple traits [#498](https://github.com/okuramasafumi/alba/issues/498) and [#502](https://github.com/okuramasafumi/alba/pull/502)
|
|
20
|
+
- Thank you for reporting this issue, @yukihiro-numata
|
|
21
|
+
- Pass params to resource in serialize when with is given [#524](https://github.com/okuramasafumi/alba/pull/524)
|
|
22
|
+
- Thank you, @JohnnyKei
|
|
23
|
+
- `Alba.serialize(single, with: SomeResource)` crash
|
|
24
|
+
- helpers are now available in NestedAttribute [#503](https://github.com/okuramasafumi/alba/pull/503)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- More strict types
|
|
29
|
+
|
|
9
30
|
## 3.10.0 2025-11-11
|
|
10
31
|
|
|
11
32
|
### Added
|
data/README.md
CHANGED
|
@@ -56,6 +56,12 @@ UserResource.new(user).serialize
|
|
|
56
56
|
|
|
57
57
|
Seems useful? Continue reading!
|
|
58
58
|
|
|
59
|
+
Pro tip: You can find more examples in separate Ruby files under "examples" directory.
|
|
60
|
+
|
|
61
|
+
### Coming from ActiveModelSerializers(AMS)?
|
|
62
|
+
|
|
63
|
+
If you are using ActiveModelSerializers and interested in Alba, we have an automatic converter! [alba_migration](https://github.com/ShoheiMitani/alba_migration) gem from [ShoheiMitani](https://github.com/ShoheiMitani) "provides a command-line tool to convert AMS serializer classes to Alba resource classes, making your migration process easier and less error-prone."
|
|
64
|
+
|
|
59
65
|
## Discussions
|
|
60
66
|
|
|
61
67
|
Alba uses [GitHub Discussions](https://github.com/okuramasafumi/alba/discussions) to openly discuss the project.
|
|
@@ -100,6 +106,7 @@ While Alba's core is simple, it provides additional features when you need them.
|
|
|
100
106
|
- Well tested, the test coverage is 99%
|
|
101
107
|
- Well maintained, getting frequent update and new releases (see [version history](https://rubygems.org/gems/alba/versions))
|
|
102
108
|
|
|
109
|
+
|
|
103
110
|
## Comparison with other serializers
|
|
104
111
|
|
|
105
112
|
Alba aims to provide a well-balanced combination of simplicity, performance, and features. Here's how it compares to other popular Ruby JSON serializers:
|
|
@@ -138,7 +145,7 @@ Or install it yourself as:
|
|
|
138
145
|
|
|
139
146
|
## Supported Ruby versions
|
|
140
147
|
|
|
141
|
-
Alba
|
|
148
|
+
Alba works with CRuby 3.0 and higher and latest JRuby and TruffleRuby. However, Ruby 3.0 is not actively tested, so use it at your own risk.
|
|
142
149
|
|
|
143
150
|
## Documentation
|
|
144
151
|
|
|
@@ -228,6 +235,20 @@ else
|
|
|
228
235
|
end
|
|
229
236
|
```
|
|
230
237
|
|
|
238
|
+
#### Default superclass configuration
|
|
239
|
+
|
|
240
|
+
You can set the default superclass for resource classes created for Nested Attributes and Traits.
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
Alba.default_superclass = MyBaseSerializer
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Note that in some environments like Rails applications, the class object is not loaded in the initializer phase. You can also set `String`.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
Alba.default_superclass = 'MyBaseSerializer'
|
|
250
|
+
```
|
|
251
|
+
|
|
231
252
|
### Naming
|
|
232
253
|
|
|
233
254
|
Alba tries to infer resource name from class name like the following.
|
|
@@ -349,7 +370,7 @@ end
|
|
|
349
370
|
class FooResource
|
|
350
371
|
include Alba::Resource
|
|
351
372
|
|
|
352
|
-
attributes :
|
|
373
|
+
attributes :format
|
|
353
374
|
|
|
354
375
|
# Here, `format` method is available
|
|
355
376
|
end
|
|
@@ -1921,16 +1942,18 @@ Don't forget calling `super` in this way.
|
|
|
1921
1942
|
|
|
1922
1943
|
### Treating specific classes as non-collection
|
|
1923
1944
|
|
|
1924
|
-
Sometimes we need to serialize an object that's `Enumerable` but not a collection. By default, Alba treats `Hash`, `Range` and `Struct` as non-collection
|
|
1945
|
+
Sometimes we need to serialize an object that's `Enumerable` but not a collection. By default, Alba treats `Hash`, `Range` and `Struct` as non-collection objects. You can add additional classes to this list via `Alba.non_collection_types`:
|
|
1925
1946
|
|
|
1926
1947
|
```ruby
|
|
1927
|
-
Alba.
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1948
|
+
Alba.non_collection_types << Stripe::StripeObject
|
|
1949
|
+
Alba.non_collection_types << MyEnumerableEntity
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
You can inspect the current list of excluded types (including the defaults) at any time:
|
|
1953
|
+
|
|
1954
|
+
```ruby
|
|
1955
|
+
Alba.non_collection_types
|
|
1956
|
+
# => [Struct, Range, Hash, Stripe::StripeObject, MyEnumerableEntity]
|
|
1934
1957
|
```
|
|
1935
1958
|
|
|
1936
1959
|
### Adding indexes to `many` association
|
data/lib/alba/association.rb
CHANGED
|
@@ -101,7 +101,7 @@ module Alba
|
|
|
101
101
|
|
|
102
102
|
def assign_resource(nesting, key_transformation, block, helper)
|
|
103
103
|
@resource = if block
|
|
104
|
-
|
|
104
|
+
Alba.resource_class(helper: helper, key_transformation: key_transformation, &block)
|
|
105
105
|
elsif Alba.inflector
|
|
106
106
|
Alba.infer_resource_class(@name, nesting: nesting)
|
|
107
107
|
else
|
|
@@ -109,14 +109,6 @@ module Alba
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def charged_resource_class(helper, key_transformation, block)
|
|
113
|
-
klass = Alba.resource_class
|
|
114
|
-
klass.helper(helper) if helper
|
|
115
|
-
klass.transform_keys(key_transformation)
|
|
116
|
-
klass.class_eval(&block)
|
|
117
|
-
klass
|
|
118
|
-
end
|
|
119
|
-
|
|
120
112
|
def to_h_with_each_resource(object, within, params)
|
|
121
113
|
object.map do |item|
|
|
122
114
|
@resource.call(item).new(item, within: within, params: params, with_traits: @with_traits).to_h
|
|
@@ -7,9 +7,11 @@ module Alba
|
|
|
7
7
|
# Setter for key_transformation, used when it's changed after class definition
|
|
8
8
|
attr_writer :key_transformation
|
|
9
9
|
|
|
10
|
+
# @param klass [Class<Alba::Resource>] the parent for this nested attribute
|
|
10
11
|
# @param key_transformation [Symbol] determines how to transform keys
|
|
11
12
|
# @param block [Proc] class body
|
|
12
|
-
def initialize(key_transformation: :none, &block)
|
|
13
|
+
def initialize(klass:, key_transformation: :none, &block)
|
|
14
|
+
@klass = klass
|
|
13
15
|
@key_transformation = key_transformation
|
|
14
16
|
@block = block
|
|
15
17
|
end
|
|
@@ -17,13 +19,13 @@ module Alba
|
|
|
17
19
|
# @param object [Object] the object being serialized
|
|
18
20
|
# @param params [Hash] params Hash inherited from Resource
|
|
19
21
|
# @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
|
|
20
|
-
# @param select [Method] select method object from its origin
|
|
21
22
|
# @return [Hash] hash serialized from running the class body in the object
|
|
22
|
-
def value(object:, params:, within
|
|
23
|
-
resource_class =
|
|
23
|
+
def value(object:, params:, within:)
|
|
24
|
+
resource_class = Class.new(@klass)
|
|
25
|
+
resource_class.instance_variable_set(:@_attributes, {}) # reset
|
|
24
26
|
resource_class.transform_keys(@key_transformation)
|
|
25
27
|
resource_class.class_eval(&@block)
|
|
26
|
-
resource_class.new(object, params: params, within: within
|
|
28
|
+
resource_class.new(object, params: params, within: within).serializable_hash
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
data/lib/alba/railtie.rb
CHANGED
|
@@ -7,13 +7,13 @@ module Alba
|
|
|
7
7
|
Alba.inflector = :active_support
|
|
8
8
|
|
|
9
9
|
ActiveSupport.on_load(:action_controller) do
|
|
10
|
-
define_method(:serialize) do |obj, with: nil, root_key: nil, meta: {}, &block|
|
|
11
|
-
resource = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
|
|
10
|
+
define_method(:serialize) do |obj, params: {}, with: nil, root_key: nil, meta: {}, &block|
|
|
11
|
+
resource = with.nil? ? Alba.resource_for(obj, params: params, &block) : with.new(obj, params: params)
|
|
12
12
|
resource.to_json(root_key: root_key, meta: meta)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
define_method(:render_serialized_json) do |obj, with: nil, root_key: nil, meta: {}, &block|
|
|
16
|
-
json = with.nil? ? Alba.resource_for(obj, &block) : with.new(obj)
|
|
15
|
+
define_method(:render_serialized_json) do |obj, params: {}, with: nil, root_key: nil, meta: {}, &block|
|
|
16
|
+
json = with.nil? ? Alba.resource_for(obj, params: params, &block) : with.new(obj, params: params)
|
|
17
17
|
render json: json.to_json(root_key: root_key, meta: meta)
|
|
18
18
|
end
|
|
19
19
|
end
|
data/lib/alba/resource.rb
CHANGED
|
@@ -51,15 +51,13 @@ module Alba
|
|
|
51
51
|
# @param within [Alba::WITHIN_DEFAULT, Hash, Array, nil, false, true]
|
|
52
52
|
# determines what associations to be serialized. If not set, it serializes all associations.
|
|
53
53
|
# @param with_traits [Symbol, Array<Symbol>, nil] specified traits
|
|
54
|
-
# @param select [Method]
|
|
54
|
+
# @param select [Method] DEPRECATED noop
|
|
55
55
|
def initialize(object, params: EMPTY_HASH, within: WITHIN_DEFAULT, with_traits: nil, select: nil)
|
|
56
56
|
@object = object
|
|
57
57
|
@params = params
|
|
58
58
|
@within = within
|
|
59
59
|
@with_traits = with_traits
|
|
60
|
-
|
|
61
|
-
# Trait and NestedAttribute generates anonymous class so it checks if it's anonymous class to prevent accidental overriding
|
|
62
|
-
self.class.define_method(:select, &select) if select && self.class.name.nil?
|
|
60
|
+
warn '`select` keyword for Alba::Resource is deprecated', category: :deprecated, uplevel: 1 if select
|
|
63
61
|
_setup
|
|
64
62
|
end
|
|
65
63
|
|
|
@@ -122,9 +120,10 @@ module Alba
|
|
|
122
120
|
Array(@with_traits).each_with_object({}) do |trait, hash|
|
|
123
121
|
body = @_traits.fetch(trait) { raise Alba::Error, "Trait not found: #{trait}" }
|
|
124
122
|
resource_class = Class.new(self.class)
|
|
123
|
+
resource_class.instance_variable_set(:@_attributes, {})
|
|
125
124
|
resource_class.class_eval(&body)
|
|
126
125
|
resource_class.transform_keys(@_transform_type) unless @_transform_type == :none
|
|
127
|
-
hash.merge!(resource_class.new(obj, params: params, within: @within
|
|
126
|
+
hash.merge!(resource_class.new(obj, params: params, within: @within).serializable_hash)
|
|
128
127
|
end
|
|
129
128
|
end
|
|
130
129
|
|
|
@@ -294,7 +293,7 @@ module Alba
|
|
|
294
293
|
when Proc then instance_exec(obj, &attribute)
|
|
295
294
|
when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(obj, params: params, within: within) }
|
|
296
295
|
when TypedAttribute then attribute.value(object: obj) { |attr| fetch_attribute(obj, key, attr) }
|
|
297
|
-
when NestedAttribute then attribute.value(object: obj, params: params, within: @within
|
|
296
|
+
when NestedAttribute then attribute.value(object: obj, params: params, within: @within)
|
|
298
297
|
when ConditionalAttribute then attribute.with_passing_condition(resource: self, object: obj) { |attr| fetch_attribute(obj, key, attr) }
|
|
299
298
|
# :nocov:
|
|
300
299
|
else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
|
@@ -488,7 +487,7 @@ module Alba
|
|
|
488
487
|
raise ArgumentError, 'No block given in attribute method' unless block
|
|
489
488
|
|
|
490
489
|
key_transformation = @_key_transformation_cascade ? @_transform_type : :none
|
|
491
|
-
attribute = NestedAttribute.new(key_transformation: key_transformation, &block)
|
|
490
|
+
attribute = NestedAttribute.new(klass: self, key_transformation: key_transformation, &block)
|
|
492
491
|
@_attributes[name.to_sym] = options[:if] ? ConditionalAttribute.new(body: attribute, condition: options[:if]) : attribute
|
|
493
492
|
end
|
|
494
493
|
alias nested nested_attribute
|
|
@@ -633,7 +632,8 @@ module Alba
|
|
|
633
632
|
# @return [void]
|
|
634
633
|
def helper(mod = @_helper || Module.new, &block)
|
|
635
634
|
mod.module_eval(&block) if block
|
|
636
|
-
|
|
635
|
+
include(mod)
|
|
636
|
+
extend(mod)
|
|
637
637
|
|
|
638
638
|
@_helper = mod
|
|
639
639
|
end
|
data/lib/alba/version.rb
CHANGED
data/lib/alba.rb
CHANGED
|
@@ -16,6 +16,16 @@ module Alba
|
|
|
16
16
|
# Getter for inflector, a module responsible for inflecting strings
|
|
17
17
|
attr_reader :inflector
|
|
18
18
|
|
|
19
|
+
# @return [Array<Class>] classes that include Enumerable but should not be treated as collections
|
|
20
|
+
attr_reader :non_collection_types
|
|
21
|
+
|
|
22
|
+
# Set the default superclass for resource classes created with {.resource_class}
|
|
23
|
+
#
|
|
24
|
+
# @param value [Class, String, Symbol] the default superclass
|
|
25
|
+
# @example
|
|
26
|
+
# Alba.default_superclass = '::MyApp::BaseResource'
|
|
27
|
+
attr_writer :default_superclass
|
|
28
|
+
|
|
19
29
|
# Set the backend, which actually serializes object into JSON
|
|
20
30
|
#
|
|
21
31
|
# @param backend [#to_sym, nil] the name of the backend
|
|
@@ -54,7 +64,7 @@ module Alba
|
|
|
54
64
|
h = hashify_collection(object, with, root_key, &block)
|
|
55
65
|
Alba.encoder.call(h)
|
|
56
66
|
else
|
|
57
|
-
resource = resource_for(object, &block)
|
|
67
|
+
resource = resource_for(object, with: with, &block)
|
|
58
68
|
resource.serialize(root_key: root_key)
|
|
59
69
|
end
|
|
60
70
|
end
|
|
@@ -73,17 +83,18 @@ module Alba
|
|
|
73
83
|
if collection?(object)
|
|
74
84
|
hashify_collection(object, with, root_key, &block)
|
|
75
85
|
else
|
|
76
|
-
resource = resource_for(object, &block)
|
|
86
|
+
resource = resource_for(object, with: with, &block)
|
|
77
87
|
resource.as_json(root_key: root_key)
|
|
78
88
|
end
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
# Detect if object is a collection or not.
|
|
82
|
-
#
|
|
92
|
+
# Types in {.non_collection_types} (default: Struct, Range, Hash) are
|
|
93
|
+
# considered non-collection even if they include Enumerable.
|
|
83
94
|
#
|
|
84
95
|
# @api private
|
|
85
96
|
def collection?(object)
|
|
86
|
-
object.is_a?(Enumerable) &&
|
|
97
|
+
object.is_a?(Enumerable) && @non_collection_types.none? { |type| object.is_a?(type) }
|
|
87
98
|
end
|
|
88
99
|
|
|
89
100
|
# Enable inference for key and resource name
|
|
@@ -124,11 +135,15 @@ module Alba
|
|
|
124
135
|
reset_transform_keys
|
|
125
136
|
end
|
|
126
137
|
|
|
138
|
+
# @param helper [Module] helper module to include
|
|
139
|
+
# @param key_transformation [Symbol] key transformation type
|
|
127
140
|
# @param block [Block] resource body
|
|
128
141
|
# @return [Class<Alba::Resource>] resource class
|
|
129
|
-
def resource_class(&block)
|
|
130
|
-
klass = Class.new
|
|
142
|
+
def resource_class(helper: nil, key_transformation: :none, &block)
|
|
143
|
+
klass = Class.new(resolved_default_superclass)
|
|
131
144
|
klass.include(Alba::Resource)
|
|
145
|
+
klass.helper(helper) if helper
|
|
146
|
+
klass.transform_keys(key_transformation)
|
|
132
147
|
klass.class_eval(&block) if block
|
|
133
148
|
klass
|
|
134
149
|
end
|
|
@@ -219,6 +234,8 @@ module Alba
|
|
|
219
234
|
@_on_error = :raise
|
|
220
235
|
@_on_nil = nil
|
|
221
236
|
@types = {}
|
|
237
|
+
@non_collection_types = [Struct, Range, Hash]
|
|
238
|
+
@default_superclass = ::Object
|
|
222
239
|
reset_transform_keys
|
|
223
240
|
register_default_types
|
|
224
241
|
end
|
|
@@ -234,6 +251,7 @@ module Alba
|
|
|
234
251
|
# Otherwise, it behaves depending on `with` argument
|
|
235
252
|
#
|
|
236
253
|
# @param object [Object] the object whose class name is used for inferring resource class
|
|
254
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
|
237
255
|
# @param with [:inference, Proc, Class<Alba::Resource>] determines how to get resource class for `object`
|
|
238
256
|
# When it's `:inference`, it infers resource class from `object`'s class name
|
|
239
257
|
# When it's a Proc, it calls the Proc with `object` as an argument
|
|
@@ -241,13 +259,13 @@ module Alba
|
|
|
241
259
|
# Otherwise, it raises an ArgumentError
|
|
242
260
|
# @return [Alba::Resource] resource class with `object` as its target object
|
|
243
261
|
# @raise [ArgumentError] if `with` argument is not one of `:inference`, Proc or Class
|
|
244
|
-
def resource_for(object, with: :inference, &block)
|
|
245
|
-
_resource_for(object, with: with, &block)
|
|
262
|
+
def resource_for(object, params: {}, with: :inference, &block)
|
|
263
|
+
_resource_for(object, params: params, with: with, &block)
|
|
246
264
|
end
|
|
247
265
|
|
|
248
266
|
private
|
|
249
267
|
|
|
250
|
-
def _resource_for(object, with: :inference, &block) # rubocop:disable Metrics/MethodLength
|
|
268
|
+
def _resource_for(object, params: {}, with: :inference, &block) # rubocop:disable Metrics/MethodLength
|
|
251
269
|
klass = if block
|
|
252
270
|
resource_class(&block)
|
|
253
271
|
else
|
|
@@ -259,7 +277,7 @@ module Alba
|
|
|
259
277
|
end
|
|
260
278
|
end
|
|
261
279
|
|
|
262
|
-
klass.new(object)
|
|
280
|
+
klass.new(object, params: params)
|
|
263
281
|
end
|
|
264
282
|
|
|
265
283
|
def inflector_from(name_or_module)
|
|
@@ -336,6 +354,10 @@ module Alba
|
|
|
336
354
|
@_transformed_keys = Hash.new { |h, k| h[k] = {} }
|
|
337
355
|
end
|
|
338
356
|
|
|
357
|
+
def resolved_default_superclass
|
|
358
|
+
@default_superclass.is_a?(Class) ? @default_superclass : Object.const_get(@default_superclass.to_s)
|
|
359
|
+
end
|
|
360
|
+
|
|
339
361
|
def register_default_types # rubocop:disable Metrics/AbcSize
|
|
340
362
|
[String, :String].each do |t|
|
|
341
363
|
register_type(t, check: ->(obj) { obj.is_a?(String) }, converter: lambda(&:to_s))
|
|
@@ -345,7 +367,7 @@ module Alba
|
|
|
345
367
|
end
|
|
346
368
|
register_type(:Boolean, check: ->(obj) { [true, false].include?(obj) }, converter: ->(obj) { !!obj })
|
|
347
369
|
[String, Integer].each do |t|
|
|
348
|
-
register_type(:"ArrayOf#{t}", check: ->(d) { d.is_a?(Array) && d.all?
|
|
370
|
+
register_type(:"ArrayOf#{t}", check: ->(d) { d.is_a?(Array) && d.all?(t) })
|
|
349
371
|
end
|
|
350
372
|
end
|
|
351
373
|
end
|