serega 0.42.0 → 0.50.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
@@ -4,162 +4,55 @@ class Serega
4
4
  #
5
5
  # Combines attribute and nested attributes
6
6
  #
7
- # @private
8
- class SeregaPlanPoint
9
- #
10
- # SeregaPlanPoint instance methods
11
- #
12
- # @private
13
- module InstanceMethods
14
- # Link to current plan this point belongs to
15
- # @return [SeregaAttribute] Current plan
16
- attr_reader :plan
17
-
18
- # Shows current attribute
19
- # @return [SeregaAttribute] Current attribute
20
- attr_reader :attribute
21
-
22
- # Shows child plan if exists
23
- # @return [SeregaPlan, nil] Attribute serialization plan
24
- attr_reader :child_plan
25
-
26
- # Child fields to serialize
27
- # @return [Hash] Attributes to serialize
28
- attr_reader :modifiers
29
-
30
- #
31
- # Initializes plan point
32
- #
33
- # @param plan [SeregaPlan] Current plan this point belongs to
34
- # @param attribute [SeregaAttribute] Attribute to construct plan point
35
- # @param modifiers Serialization parameters
36
- # @option modifiers [Hash] :only The only attributes to serialize
37
- # @option modifiers [Hash] :except Attributes to hide
38
- # @option modifiers [Hash] :with Hidden attributes to serialize additionally
39
- #
40
- # @return [SeregaPlanPoint] New plan point
41
- #
42
- def initialize(plan, attribute, modifiers = nil)
43
- @plan = plan
44
- @attribute = attribute
45
- @modifiers = modifiers
46
- set_normalized_vars
47
- end
48
-
49
- # Attribute `value`
50
- # @see SeregaAttribute::AttributeInstanceMethods#value
51
- def value(obj, ctx, batches: nil)
52
- attribute.value(obj, ctx, batches: batches)
53
- end
54
-
55
- # Attribute `name`
56
- # @see SeregaAttribute::AttributeInstanceMethods#value
57
- def name
58
- attribute.name
59
- end
60
-
61
- # Attribute `many` option
62
- # @see SeregaAttribute::AttributeInstanceMethods#many
63
- def many
64
- attribute.many
65
- end
66
-
67
- # Attribute `serializer` option
68
- # @see SeregaAttribute::AttributeInstanceMethods#serializer
69
- def serializer
70
- attribute.serializer
71
- end
72
-
73
- # Attribute `batch_loaders`
74
- # @see SeregaAttribute::AttributeInstanceMethods#batch_loaders
75
- def batch_loaders
76
- attribute.batch_loaders
77
- end
78
-
79
- # Attribute `preloads`
80
- # @see SeregaAttribute::AttributeInstanceMethods#preloads
81
- def preloads
82
- attribute.preloads
83
- end
84
-
85
- # Runs this point's declared preloads over the given objects using the
86
- # serializer's registered preload handler.
87
- #
88
- # Presenters are unwrapped first, as preload handlers work with the
89
- # serialized objects themselves.
90
- #
91
- # @param objects [Array] objects serialized at this point's level
92
- # @return [void]
93
- def run_preloads(objects)
94
- return unless preloads
95
-
96
- serializer_class = self.class.serializer_class
97
- objects = objects.map(&:__getobj__) if serializer_class.presenter
98
-
99
- handler = serializer_class.preload_with
100
- unless handler
101
- raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
102
- end
103
-
104
- handler.call(objects, preloads)
105
- rescue => error
106
- SeregaUtils::SerializedAttributeError.call(error, self)
107
- end
108
-
109
- # Loads the batch loaders this point's value needs, each once for the whole
110
- # level, and returns them keyed by loader name for #value to read from.
111
- #
112
- # @param level [SeregaEngine::Level] level whose objects are loaded for
113
- # @return [Hash, nil] loaded data per loader name, or nil when none are needed
114
- def load_batches(level)
115
- names = batch_loaders
116
- return if names.empty?
117
-
118
- loaders = self.class.serializer_class.batch_loaders
119
- names.each_with_object({}) do |name, batches|
120
- batches[name] = level.fetch(loaders[name])
121
- end
122
- rescue => error
123
- SeregaUtils::SerializedAttributeError.call(error, self)
124
- end
125
-
126
- #
127
- # @return [Class<SeregaObjectSerializer>] object serializer class for child plan
128
- #
129
- def child_object_serializer
130
- serializer::SeregaObjectSerializer
131
- end
132
-
133
- # Builds the object serializer that serializes this point's relation, or nil
134
- # when the point has no child plan (a plain attribute). The point owns the
135
- # static config (child plan, serializer class, `many`); the caller injects the
136
- # runtime `context` and `opts`.
137
- #
138
- # @param context [Hash] serialization context
139
- # @param opts [Hash] extra object-serializer options (e.g. the level queue)
140
- # @return [SeregaObjectSerializer, nil] serializer for the child level
141
- def child_serializer(context:, **opts)
142
- return unless child_plan
143
-
144
- child_object_serializer.new(context: context, plan: child_plan, many: many, **opts)
145
- end
146
-
147
- private
7
+ class SeregaPlanPoint # :nodoc:
8
+ # Link to current plan this point belongs to
9
+ attr_reader :plan
10
+
11
+ # Shows current attribute
12
+ attr_reader :attribute
13
+
14
+ # Attribute `name`
15
+ attr_reader :name
16
+
17
+ # Shows child plan if exists
18
+ attr_reader :child_plan
19
+
20
+ # Whether the relation values of a class are collections, for a point
21
+ # with a child plan
22
+ attr_reader :collection_classes
23
+
24
+ # Initializes plan point
25
+ def initialize(plan, attribute, modifiers = nil)
26
+ @plan = plan
27
+ @attribute = attribute
28
+ @name = attribute.name
29
+ @child_plan = serializer ? SeregaPlan.new(serializer, self, modifiers || FROZEN_EMPTY_HASH) : nil
30
+ @collection_classes = @child_plan ? {}.compare_by_identity : nil
31
+ end
148
32
 
149
- def set_normalized_vars
150
- @child_plan = prepare_child_plan
151
- end
33
+ # Attribute `many` option
34
+ def many
35
+ attribute.many
36
+ end
152
37
 
153
- def prepare_child_plan
154
- return unless serializer
38
+ # Attribute `serializer` option
39
+ def serializer
40
+ attribute.serializer
41
+ end
155
42
 
156
- fields = modifiers || FROZEN_EMPTY_HASH
43
+ # Attribute `batch_loaders`
44
+ def batch_loaders
45
+ attribute.batch_loaders
46
+ end
157
47
 
158
- serializer::SeregaPlan.new(self, fields)
159
- end
48
+ # Attribute `preloads`
49
+ def preloads
50
+ attribute.preloads
160
51
  end
161
52
 
162
- extend SeregaHelpers::SerializerClassHelper
163
- include InstanceMethods
53
+ # Attribute `conditional?`
54
+ def conditional?
55
+ attribute.conditional?
56
+ end
164
57
  end
165
58
  end
@@ -2,97 +2,30 @@
2
2
 
3
3
  class Serega
4
4
  module SeregaPlugins
5
+ # `plugin :activerecord_preloads` loads every `:preload` of the serialized
6
+ # fields once, with `ActiveRecord::Associations::Preloader`
5
7
  #
6
- # Plugin :activerecord_preloads
7
- #
8
- # Automatically preloads associations to serialized objects
9
- #
10
- # Every association declared with `:preload` is loaded once during serialization using
11
- # ActiveRecord::Associations::Preloader, so there are no N+1 queries.
12
- #
13
- # @example
14
- # class AppSerializer < Serega
15
- # config.auto_preload = true
16
- # plugin :activerecord_preloads
17
- # end
18
- #
19
- # class AlbumSerializer < AppSerializer
20
- # # no preloads
21
- # attribute :title
22
- #
23
- # # preloads :downloads, as manually specified
24
- # attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
25
- # end
26
- #
27
- # class UserSerializer < AppSerializer
28
- # # no preloads
29
- # attribute :username
30
- #
31
- # # preloads :user_stats, as auto_preload is enabled for :delegate attributes
32
- # attribute :comments_count, delegate: { to: :user_stats }
33
- #
34
- # # preloads :albums, as auto_preload is enabled for :serializer attributes
35
- # attribute :albums, serializer: AlbumSerializer
36
- # end
37
- #
38
- # UserSerializer.to_h(users)
39
- # # 1 query to load :user_stats for all users
40
- # # + 1 query to load :albums for all users
41
- # # + 1 query to load :downloads for all albums
42
- # # = 3 queries total, regardless of how many users/albums are serialized
8
+ # ```ruby
9
+ # class AppSerializer < Serega
10
+ # plugin :activerecord_preloads
11
+ # config.auto_preload = true
12
+ # end
13
+ # ```
43
14
  #
15
+ # See [ActiveRecord preloads](https://github.com/andreyruby/serega/blob/master/docs/preloads.md#activerecord).
44
16
  module ActiverecordPreloads
45
- #
46
- # @return [Symbol] Plugin name
47
- #
48
- # @private
49
- def self.plugin_name
50
- :activerecord_preloads
51
- end
52
-
53
- # Checks requirements to load plugin
54
- #
55
- # @param serializer_class [Class<Serega>] Current serializer class
56
- # @param opts [Hash] plugin options
57
- #
58
- # @return [void]
59
- #
60
- # @private
61
- def self.before_load_plugin(serializer_class, **opts)
62
- opts.each_key do |key|
63
- raise SeregaError, "Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. No options are allowed"
17
+ # Adds the plugin to the serializer
18
+ def self.apply(serializer_class, **opts) # :nodoc:
19
+ unless opts.empty?
20
+ raise SeregaError, "Plugin :activerecord_preloads does not accept the #{opts.keys.first.inspect} option. No options are allowed"
64
21
  end
65
- end
66
22
 
67
- #
68
- # Applies plugin code to specific serializer
69
- #
70
- # @param serializer_class [Class<Serega>] Current serializer class
71
- # @param _opts [Hash] Plugin options
72
- #
73
- # @return [void]
74
- #
75
- # @private
76
- def self.load_plugin(serializer_class, **_opts)
77
23
  require_relative "lib/preloader"
78
- end
79
24
 
80
- #
81
- # Registers the ActiveRecord preload handler
82
- #
83
- # @param serializer_class [Class<Serega>] Current serializer class
84
- # @param _opts [Hash] Plugin options
85
- #
86
- # @return [void]
87
- #
88
- # @private
89
- def self.after_load_plugin(serializer_class, **_opts)
90
25
  serializer_class.preload_with do |objects, preloads|
91
26
  Preloader.preload(objects, preloads)
92
27
  end
93
28
  end
94
29
  end
95
-
96
- register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
97
30
  end
98
31
  end
@@ -4,16 +4,9 @@ class Serega
4
4
  module SeregaPlugins
5
5
  module ActiverecordPreloads
6
6
  # Handles preloads for different types of initial records
7
- # @private
8
- class Preloader
7
+ class Preloader # :nodoc:
9
8
  class << self
10
9
  # Preloads associations to records
11
- #
12
- # @param object [Object] record(s)
13
- # @param preloads [Hash] associations names
14
- #
15
- # @return [Object] provided object with preloaded associations
16
- #
17
10
  def preload(object, preloads)
18
11
  return object if object.nil? || (object.is_a?(Array) && object.empty?)
19
12
 
@@ -23,9 +16,7 @@ class Serega
23
16
  preload_handler.preload(object, preloads)
24
17
  end
25
18
 
26
- # Returns handlers which will try to check if serialized object fits for preloading using this handler
27
- #
28
- # @return [Array] Registered preload adapters for different types of initial records
19
+ # Returns handlers which will try to check if the object to serialize fits for preloading using this handler
29
20
  def handlers
30
21
  @handlers ||= [
31
22
  ActiverecordRelation,
@@ -38,47 +29,22 @@ class Serega
38
29
  end
39
30
 
40
31
  # Associations loader for prepared records
41
- # @private
42
- class Loader
43
- # :nocov: We can check only one version of activerecord
44
-
32
+ class Loader # :nodoc:
45
33
  # Preloads associations to records
46
- #
47
- # @param records [Array<ActiveRecord::Base>] records
48
- # @param associations [Hash] associations names
49
- #
50
- # @return [void]
51
- #
52
34
  def self.call(records, associations)
53
- if ActiveRecord::VERSION::MAJOR >= 7
54
- ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
55
- else
56
- ActiveRecord::Associations::Preloader.new.preload(records, associations)
57
- end
35
+ ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
58
36
  end
59
- # :nocov:
60
37
  end
61
38
 
62
39
  # Preloader adapter for ActiveRecord object
63
- # @private
64
- class ActiverecordObject
40
+ class ActiverecordObject # :nodoc:
65
41
  class << self
66
- #
67
42
  # Checks object is kind of ActiveRecord::Base
68
- #
69
- # @param object [Object] object
70
- #
71
- # @return [Boolean] whether object is kind of ActiveRecord::Base
72
43
  def fit?(object)
73
44
  object.is_a?(ActiveRecord::Base)
74
45
  end
75
46
 
76
- #
77
47
  # Preloads associations to ActiveRecord::Base record
78
- #
79
- # @param record [ActiveRecord::Base] record
80
- #
81
- # @return [Object] provided record with preloaded associations
82
48
  def preload(record, preloads)
83
49
  Loader.call([record], preloads)
84
50
  record
@@ -87,25 +53,14 @@ class Serega
87
53
  end
88
54
 
89
55
  # Preloader adapter for ActiveRecord::Relation
90
- # @private
91
- class ActiverecordRelation
56
+ class ActiverecordRelation # :nodoc:
92
57
  class << self
93
- #
94
58
  # Checks object is kind of ActiveRecord::Relation
95
- #
96
- # @param object [Object] object to check
97
- #
98
- # @return [Boolean] whether object is kind of ActiveRecord::Relation
99
59
  def fit?(object)
100
60
  object.is_a?(ActiveRecord::Relation)
101
61
  end
102
62
 
103
- #
104
63
  # Preloads associations to ActiveRecord::Relation
105
- #
106
- # @param scope [ActiveRecord::Relation] scope
107
- #
108
- # @return [ActiveRecord::Relation] provided scope with preloaded associations
109
64
  def preload(scope, preloads)
110
65
  scope.load
111
66
  Loader.call(scope.to_a, preloads)
@@ -115,27 +70,16 @@ class Serega
115
70
  end
116
71
 
117
72
  # Preloader adapter for Array of ActiveRecord objects
118
- # @private
119
- class ActiverecordArray
73
+ class ActiverecordArray # :nodoc:
120
74
  class << self
121
- #
122
75
  # Checks object is an array of ActiveRecord::Base objects
123
- #
124
- # @param object [Object] object
125
- #
126
- # @return [Boolean] whether object is an array with ActiveRecord objects (and all objects have same class)
127
76
  def fit?(object)
128
77
  object.is_a?(Array) &&
129
78
  ActiverecordObject.fit?(object.first) &&
130
79
  same_kind?(object)
131
80
  end
132
81
 
133
- #
134
82
  # Preloads associations to array with ActiveRecord::Base objects
135
- #
136
- # @param records [Array<ActiveRecord::Base>] ActiveRecord records
137
- #
138
- # @return [Array<ActiveRecord::Base>] provided records with preloaded associations
139
83
  def preload(records, preloads)
140
84
  Loader.call(records, preloads)
141
85
  records
@@ -151,26 +95,15 @@ class Serega
151
95
  end
152
96
 
153
97
  # Preloader adapter for Enumerator with ActiveRecord objects
154
- # @private
155
- class ActiverecordEnumerator
98
+ class ActiverecordEnumerator # :nodoc:
156
99
  class << self
157
- #
158
100
  # Checks object is an Enumerator with each value is a ActiveRecord::Base object
159
- #
160
- # @param object [Object] object
161
- #
162
- # @return [Boolean] whether object is an Enumerator with each value is a ActiveRecord::Base object
163
101
  def fit?(object)
164
102
  object.is_a?(Enumerator) &&
165
103
  ActiverecordArray.fit?(object.to_a)
166
104
  end
167
105
 
168
- #
169
106
  # Preloads associations to Enumerator with ActiveRecord::Base objects
170
- #
171
- # @param enum [Enumerator<ActiveRecord::Base>] enum
172
- #
173
- # @return [Enumerator<ActiveRecord::Base>] provided enumerator with preloaded associations
174
107
  def preload(enum, preloads)
175
108
  ActiverecordArray.preload(enum.to_a, preloads)
176
109
  enum