elasticgraph-schema_definition 0.18.0.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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,237 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "did_you_mean"
10
+ require "elastic_graph/constants"
11
+ require "elastic_graph/error"
12
+
13
+ module ElasticGraph
14
+ module SchemaDefinition
15
+ module SchemaElements
16
+ # Abstraction for generating derived GraphQL type names based on a collection of formats. A default set of formats is included, and
17
+ # overrides can be provided to customize the format we use for naming derived types.
18
+ class TypeNamer < ::Struct.new(:formats, :regexes, :name_overrides, :reverse_overrides)
19
+ # Initializes a new `TypeNamer` with the provided format overrides.
20
+ # The keys in `overrides` must match the keys in `DEFAULT_FORMATS` and the values must have
21
+ # the same placeholders as are present in the default formats.
22
+ #
23
+ # @private
24
+ def initialize(format_overrides: {}, name_overrides: {})
25
+ @used_names = []
26
+ name_overrides = name_overrides.transform_keys(&:to_s)
27
+
28
+ validate_format_overrides(format_overrides)
29
+ validate_name_overrides(name_overrides)
30
+
31
+ formats = DEFAULT_FORMATS.merge(format_overrides)
32
+ regexes = formats.transform_values { |format| /\A#{format.gsub(PLACEHOLDER_REGEX, "(\\w+)")}\z/ }
33
+ reverse_overrides = name_overrides.to_h { |k, v| [v, k] }
34
+
35
+ super(formats: formats, regexes: regexes, name_overrides: name_overrides, reverse_overrides: reverse_overrides)
36
+ end
37
+
38
+ # Returns the configured name for the given `standard_name`.
39
+ #
40
+ # By default, the returned name will just be the string form of the given `standard_name`, but if
41
+ # the `TypeNamer` was instantiated with an override for the given `standard_name`, that will be
42
+ # returned instead.
43
+ #
44
+ # @private
45
+ def name_for(standard_name)
46
+ string_name = standard_name.to_s
47
+ @used_names << string_name
48
+ name_overrides.fetch(string_name, string_name)
49
+ end
50
+
51
+ # If the given `potentially_overriden_name` is an overridden name, returns the name from before the
52
+ # override was applied. Note: this may not be the true "original" name that ElasticGraph would have
53
+ # have used (e.g. it could still be customized by `formats`) but it will be the name that would
54
+ # be used without any `name_overrides`.
55
+ #
56
+ # @private
57
+ def revert_override_for(potentially_overriden_name)
58
+ reverse_overrides.fetch(potentially_overriden_name, potentially_overriden_name)
59
+ end
60
+
61
+ # Generates a derived type name based on the provided format name and arguments. The given arguments must match
62
+ # the placeholders in the format. If the format name is unknown or the arguments are invalid, a `ConfigError` is raised.
63
+ #
64
+ # Note: this does not apply any configured `name_overrides`. It's up to the caller to apply that when desired.
65
+ #
66
+ # @private
67
+ def generate_name_for(format_name, **args)
68
+ format = formats.fetch(format_name) do
69
+ suggestions = FORMAT_SUGGESTER.correct(format_name).map(&:inspect)
70
+ raise ConfigError, "Unknown format name: #{format_name.inspect}. Possible alternatives: #{suggestions.join(", ")}."
71
+ end
72
+
73
+ expected_placeholders = REQUIRED_PLACEHOLDERS.fetch(format_name)
74
+ if (missing_placeholders = expected_placeholders - args.keys).any?
75
+ raise ConfigError, "The arguments (#{args.inspect}) provided for `#{format_name}` format (#{format.inspect}) omits required key(s): #{missing_placeholders.join(", ")}."
76
+ end
77
+
78
+ if (extra_placeholders = args.keys - expected_placeholders).any?
79
+ raise ConfigError, "The arguments (#{args.inspect}) provided for `#{format_name}` format (#{format.inspect}) contains extra key(s): #{extra_placeholders.join(", ")}."
80
+ end
81
+
82
+ format % args
83
+ end
84
+
85
+ # Given a `name` that has been generated for the given `format`, extracts the `base` parameter value that was used
86
+ # to generate `name`.
87
+ #
88
+ # Raises an error if the given `format` does not support `base` extraction. (To extract `base`, it's required that
89
+ # `base` is the only placeholder in the format.)
90
+ #
91
+ # Returns `nil` if the given `format` does support `base` extraction but `name` does not match the `format`.
92
+ #
93
+ # @private
94
+ def extract_base_from(name, format:)
95
+ unless REQUIRED_PLACEHOLDERS.fetch(format) == [:base]
96
+ raise InvalidArgumentValueError, "The `#{format}` format does not support base extraction."
97
+ end
98
+
99
+ regexes.fetch(format).match(name)&.captures&.first
100
+ end
101
+
102
+ # Indicates if the given `name` matches the format for the provided `format_name`.
103
+ #
104
+ # Note: our formats are not "mutually exclusive"--some names can match more than one format, so the
105
+ # fact that a name matches a format does not guarantee it was generated by that format.
106
+ #
107
+ # @private
108
+ def matches_format?(name, format_name)
109
+ regexes.fetch(format_name).match?(name)
110
+ end
111
+
112
+ # Returns a hash containing the entries of `name_overrides` which have not been used.
113
+ # These are likely to be typos, and they can be used to warn the user.
114
+ #
115
+ # @private
116
+ def unused_name_overrides
117
+ name_overrides.except(*@used_names.uniq)
118
+ end
119
+
120
+ # Returns a set containing all names that got passed to `name_for`: essentially, these are the
121
+ # candidates for valid name overrides.
122
+ #
123
+ # Can be used (in conjunction with `unused_name_overrides`) to provide suggested
124
+ # alternatives to the user.
125
+ #
126
+ # @private
127
+ def used_names
128
+ @used_names.to_set
129
+ end
130
+
131
+ # Extracts the names of the placeholders from the provided format.
132
+ #
133
+ # @private
134
+ def self.placeholders_in(format)
135
+ format.scan(PLACEHOLDER_REGEX).flatten.map(&:to_sym)
136
+ end
137
+
138
+ # The default formats used for derived GraphQL type names. These formats can be customized by providing `derived_type_name_formats`
139
+ # to {RakeTasks} or {Local::RakeTasks}.
140
+ #
141
+ # @return [Hash<Symbol, String>]
142
+ DEFAULT_FORMATS = {
143
+ AggregatedValues: "%{base}AggregatedValues",
144
+ Aggregation: "%{base}Aggregation",
145
+ Connection: "%{base}Connection",
146
+ Edge: "%{base}Edge",
147
+ FieldsListFilterInput: "%{base}FieldsListFilterInput",
148
+ FilterInput: "%{base}FilterInput",
149
+ GroupedBy: "%{base}GroupedBy",
150
+ InputEnum: "%{base}Input",
151
+ ListElementFilterInput: "%{base}ListElementFilterInput",
152
+ ListFilterInput: "%{base}ListFilterInput",
153
+ SortOrder: "%{base}SortOrder",
154
+ SubAggregation: "%{parent_types}%{base}SubAggregation",
155
+ SubAggregations: "%{parent_agg_type}%{field_path}SubAggregations"
156
+ }.freeze
157
+
158
+ private
159
+
160
+ # https://rubular.com/r/EJMY0zHZiC5HQm
161
+ PLACEHOLDER_REGEX = /%\{(\w+)\}/
162
+
163
+ REQUIRED_PLACEHOLDERS = DEFAULT_FORMATS.transform_values { |format| placeholders_in(format) }
164
+ FORMAT_SUGGESTER = ::DidYouMean::SpellChecker.new(dictionary: DEFAULT_FORMATS.keys)
165
+ DEFINITE_ENUM_FORMATS = [:SortOrder].to_set
166
+ DEFINITE_OBJECT_FORMATS = DEFAULT_FORMATS.keys.to_set - DEFINITE_ENUM_FORMATS - [:InputEnum].to_set
167
+ TYPES_THAT_CANNOT_BE_OVERRIDDEN = STOCK_GRAPHQL_SCALARS.union(["Query"]).freeze
168
+
169
+ def validate_format_overrides(format_overrides)
170
+ format_problems = format_overrides.flat_map do |format_name, format|
171
+ validate_format(format_name, format)
172
+ end
173
+
174
+ notify_problems(format_problems, "Provided derived type name formats")
175
+ end
176
+
177
+ def validate_format(format_name, format)
178
+ if (required_placeholders = REQUIRED_PLACEHOLDERS[format_name])
179
+ placeholders = self.class.placeholders_in(format)
180
+ placeholder_problems = [] # : ::Array[String]
181
+
182
+ if (missing_placeholders = required_placeholders - placeholders).any?
183
+ placeholder_problems << "The #{format_name} format #{format.inspect} is missing required placeholders: #{missing_placeholders.join(", ")}. " \
184
+ "Example valid format: #{DEFAULT_FORMATS.fetch(format_name).inspect}."
185
+ end
186
+
187
+ if (extra_placeholders = placeholders - required_placeholders).any?
188
+ placeholder_problems << "The #{format_name} format #{format.inspect} has excess placeholders: #{extra_placeholders.join(", ")}. " \
189
+ "Example valid format: #{DEFAULT_FORMATS.fetch(format_name).inspect}."
190
+ end
191
+
192
+ example_name = format % placeholders.to_h { |placeholder| [placeholder.to_sym, placeholder.capitalize] }
193
+ unless GRAPHQL_NAME_PATTERN.match(example_name)
194
+ placeholder_problems << "The #{format_name} format #{format.inspect} does not produce a valid GraphQL type name. " +
195
+ GRAPHQL_NAME_VALIDITY_DESCRIPTION
196
+ end
197
+
198
+ placeholder_problems
199
+ else
200
+ suggestions = FORMAT_SUGGESTER.correct(format_name).map(&:inspect)
201
+ ["Unknown format name: #{format_name.inspect}. Possible alternatives: #{suggestions.join(", ")}."]
202
+ end
203
+ end
204
+
205
+ def validate_name_overrides(name_overrides)
206
+ duplicate_problems = name_overrides
207
+ .group_by { |k, v| v }
208
+ .transform_values { |kv_pairs| kv_pairs.map(&:first) }
209
+ .select { |_, v| v.size > 1 }
210
+ .map do |override, source_names|
211
+ "Multiple names (#{source_names.sort.join(", ")}) map to the same override: #{override}, which is not supported."
212
+ end
213
+
214
+ invalid_name_problems = name_overrides.filter_map do |source_name, override|
215
+ unless GRAPHQL_NAME_PATTERN.match(override)
216
+ "`#{override}` (the override for `#{source_name}`) is not a valid GraphQL type name. " +
217
+ GRAPHQL_NAME_VALIDITY_DESCRIPTION
218
+ end
219
+ end
220
+
221
+ cant_override_problems = TYPES_THAT_CANNOT_BE_OVERRIDDEN.intersection(name_overrides.keys).map do |type_name|
222
+ "`#{type_name}` cannot be overridden because it is part of the GraphQL spec."
223
+ end
224
+
225
+ notify_problems(duplicate_problems + invalid_name_problems + cant_override_problems, "Provided type name overrides")
226
+ end
227
+
228
+ def notify_problems(problems, source_description)
229
+ return if problems.empty?
230
+
231
+ raise ConfigError, "#{source_description} have #{problems.size} problem(s):\n\n" \
232
+ "#{problems.map.with_index(1) { |problem, i| "#{i}. #{problem}" }.join("\n\n")}"
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,353 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"
11
+ require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
12
+ require "elastic_graph/schema_definition/schema_elements/type_namer"
13
+ require "elastic_graph/support/memoizable_data"
14
+ require "forwardable"
15
+
16
+ module ElasticGraph
17
+ module SchemaDefinition
18
+ module SchemaElements
19
+ # Represents a reference to a type. This is basically just a name of a type,
20
+ # with the ability to resolve it to an actual type object on demand. In addition,
21
+ # we provide some useful logic that is based entirely on the type name.
22
+ #
23
+ # This is necessary because GraphQL does not require that types are defined
24
+ # before they are referenced. (And also you can have circular type dependencies).
25
+ # Therefore, we need to use a reference to a type initially, and can later resolve
26
+ # it to a concrete type object as needed.
27
+ #
28
+ # @private
29
+ class TypeReference < Support::MemoizableData.define(:name, :schema_def_state)
30
+ extend Forwardable
31
+ # @dynamic type_namer
32
+ def_delegator :schema_def_state, :type_namer
33
+
34
+ # Extracts the type without any non-null or list wrappings it has.
35
+ def fully_unwrapped
36
+ schema_def_state.type_ref(unwrapped_name)
37
+ end
38
+
39
+ # Removes any non-null wrappings the type has.
40
+ def unwrap_non_null
41
+ schema_def_state.type_ref(name.delete_suffix("!"))
42
+ end
43
+
44
+ def wrap_non_null
45
+ return self if non_null?
46
+ schema_def_state.type_ref("#{name}!")
47
+ end
48
+
49
+ # Removes the list wrapping if this is a list.
50
+ #
51
+ # If the outer wrapping is non-null, unwraps that as well.
52
+ def unwrap_list
53
+ schema_def_state.type_ref(unwrap_non_null.name.delete_prefix("[").delete_suffix("]"))
54
+ end
55
+
56
+ # Returns the `ObjectType`, `UnionType` or `InterfaceType` object to which this
57
+ # type name refers, if it is the name of one of those kinds of types.
58
+ #
59
+ # Ignores any non-null wrapping on the type, if there is one.
60
+ def as_object_type
61
+ type = _ = unwrap_non_null.resolved
62
+ type if type.respond_to?(:graphql_fields_by_name)
63
+ end
64
+
65
+ # Returns `true` if this is known to be an object type of some sort (including interface types,
66
+ # union types, and proper object types).
67
+ #
68
+ # Returns `false` if this is known to be a leaf type of some sort (either a scalar or enum).
69
+ # Returns `false` if this is a list type (either a list of objects or leafs).
70
+ #
71
+ # Raises an error if it cannot be determined either from the name or by resolving the type.
72
+ #
73
+ # Ignores any non-null wrapping on the type, if there is one.
74
+ def object?
75
+ return unwrap_non_null.object? if non_null?
76
+
77
+ if (resolved_type = resolved)
78
+ return resolved_type.respond_to?(:graphql_fields_by_name)
79
+ end
80
+
81
+ # For derived GraphQL types, the name usually implies what kind of type it is.
82
+ # The derived types get generated last, so this prediate may be called before the
83
+ # type has been defined.
84
+ case schema_kind_implied_by_name
85
+ when :object
86
+ true
87
+ when :enum
88
+ false
89
+ else
90
+ # If we can't determine the type from the name, just raise an error.
91
+ raise SchemaError, "Type `#{name}` cannot be resolved. Is it misspelled?"
92
+ end
93
+ end
94
+
95
+ def enum?
96
+ return unwrap_non_null.enum? if non_null?
97
+
98
+ if (resolved_type = resolved)
99
+ return resolved_type.is_a?(EnumType)
100
+ end
101
+
102
+ # For derived GraphQL types, the name usually implies what kind of type it is.
103
+ # The derived types get generated last, so this prediate may be called before the
104
+ # type has been defined.
105
+ case schema_kind_implied_by_name
106
+ when :object
107
+ false
108
+ when :enum
109
+ true
110
+ else
111
+ # If we can't determine the type from the name, just raise an error.
112
+ raise SchemaError, "Type `#{name}` cannot be resolved. Is it misspelled?"
113
+ end
114
+ end
115
+
116
+ # Returns `true` if this is known to be a scalar type or enum type.
117
+ # Returns `false` if this is known to be an object type or list type of any sort.
118
+ #
119
+ # Raises an error if it cannot be determined either from the name or by resolving the type.
120
+ #
121
+ # Ignores any non-null wrapping on the type, if there is one.
122
+ def leaf?
123
+ !list? && !object?
124
+ end
125
+
126
+ # Returns `true` if this is a list type.
127
+ #
128
+ # Ignores any non-null wrapping on the type, if there is one.
129
+ def list?
130
+ name.start_with?("[")
131
+ end
132
+
133
+ # Returns `true` if this is a non-null type.
134
+ def non_null?
135
+ name.end_with?("!")
136
+ end
137
+
138
+ def boolean?
139
+ name == "Boolean"
140
+ end
141
+
142
+ def to_s
143
+ name
144
+ end
145
+
146
+ def resolved
147
+ schema_def_state.types_by_name[name]
148
+ end
149
+
150
+ def unwrapped_name
151
+ name
152
+ .sub(/\A\[+/, "") # strip `[` characters from the start: https://rubular.com/r/tHVBBQkQUMMVVz
153
+ .sub(/[\]!]+\z/, "") # strip `]` and `!` characters from the end: https://rubular.com/r/pC8C0i7EpvHDbf
154
+ end
155
+
156
+ # Generally speaking, scalar types have `grouped_by` fields which are scalars of the same types,
157
+ # and object types have `grouped_by` fields which are special `[object_type]GroupedBy` types.
158
+ #
159
+ # ...except for some special cases (Date and DateTime), which this predicate detects.
160
+ def scalar_type_needing_grouped_by_object?
161
+ %w[Date DateTime].include?(type_namer.revert_override_for(name))
162
+ end
163
+
164
+ # Returns a new `TypeReference` with any type name overrides reverted (to provide the "original" type name).
165
+ def with_reverted_override
166
+ schema_def_state.type_ref(type_namer.revert_override_for(name))
167
+ end
168
+
169
+ # Returns all the JSON schema array/nullable layers of a type, from outermost to innermost.
170
+ # For example, [[Int]] will return [:nullable, :array, :nullable, :array, :nullable]
171
+ def json_schema_layers
172
+ @json_schema_layers ||= begin
173
+ layers, inner_type = peel_json_schema_layers_once
174
+
175
+ if layers.empty? || inner_type == self
176
+ layers
177
+ else
178
+ layers + inner_type.json_schema_layers
179
+ end
180
+ end
181
+ end
182
+
183
+ # Most of ElasticGraph's derived GraphQL types have a static suffix (e.g. the full type name
184
+ # is source_type + suffix). This is a map of all of these.
185
+ STATIC_FORMAT_NAME_BY_CATEGORY = TypeNamer::REQUIRED_PLACEHOLDERS.filter_map do |format_name, placeholders|
186
+ if placeholders == [:base]
187
+ as_snake_case = SchemaArtifacts::RuntimeMetadata::SchemaElementNamesDefinition::SnakeCaseConverter
188
+ .normalize_case(format_name.to_s)
189
+ .delete_prefix("_")
190
+
191
+ [as_snake_case.to_sym, format_name]
192
+ end
193
+ end.to_h
194
+
195
+ # Converts the TypeReference to its final form (i.e. the from that will be used in rendered schema artifacts).
196
+ # This handles multiple bits of type name customization based on the configured `type_name_overrides` and
197
+ # `derived_type_name_formats` settings (via the `TypeNamer`):
198
+ #
199
+ # - If the `as_input` is `true` and this is a reference to an enum type, converts to the `InputEnum` format.
200
+ # - If there is a configured name override that applies to this type, uses it.
201
+ def to_final_form(as_input: false)
202
+ unwrapped = fully_unwrapped
203
+ inner_name = type_namer.name_for(unwrapped.name)
204
+
205
+ if as_input && schema_def_state.type_ref(inner_name).enum?
206
+ inner_name = type_namer.name_for(
207
+ type_namer.generate_name_for(:InputEnum, base: inner_name)
208
+ )
209
+ end
210
+
211
+ renamed_with_same_wrappings(inner_name)
212
+ end
213
+
214
+ # Builds a `TypeReference` for a statically named derived type for the given `category.
215
+ #
216
+ # In addition, a dynamic method `as_[category]` is also provided (defined further below).
217
+ def as_static_derived_type(category)
218
+ renamed_with_same_wrappings(type_namer.generate_name_for(
219
+ STATIC_FORMAT_NAME_BY_CATEGORY.fetch(category),
220
+ base: fully_unwrapped.name
221
+ ))
222
+ end
223
+
224
+ # Generates the type name used for a sub-aggregation. This type has `grouped_by`, `aggregated_values`,
225
+ # `count` and `sub_aggregations` sub-fields to expose the different bits of aggregation functionality.
226
+ #
227
+ # The type name is based both on the type reference name and on the set of `parent_doc_types`
228
+ # that exist above it. The `parent_doc_types` are used in the name because we plan to offer different sub-aggregations
229
+ # under it based on where it is in the document structure. A type which is `nested` at multiple levels in different
230
+ # document contexts needs separate types generated for each case so that we can offer the correct contextual
231
+ # sub-aggregations that can be offered for each case.
232
+ def as_sub_aggregation(parent_doc_types:)
233
+ renamed_with_same_wrappings(type_namer.generate_name_for(
234
+ :SubAggregation,
235
+ base: fully_unwrapped.name,
236
+ parent_types: parent_doc_types.join
237
+ ))
238
+ end
239
+
240
+ # Generates the type name used for a `sub_aggregations` field. A `sub_aggregations` field is
241
+ # available alongside `grouped_by`, `count`, and `aggregated_values` on an aggregation or
242
+ # sub-aggregation node. This type is used in two situations:
243
+ #
244
+ # 1. It is used directly under `nodes`/`edges { node }` on an Aggregation or SubAggregation.
245
+ # It provides access to each of the sub-aggregations that are available in that context.
246
+ # 2. It is used underneath that `SubAggregations` object for single object fields which have
247
+ # fields under them that are sub-aggregatable.
248
+ #
249
+ # The fields (and types of those fields) used for one of these types is contextual based on
250
+ # what the parent doc types are (so that we can offer sub-aggregations of the parent doc types!)
251
+ # and the field path (for the 2nd case).
252
+ def as_aggregation_sub_aggregations(parent_doc_types: [fully_unwrapped.name], field_path: [])
253
+ field_part = field_path.map { |f| to_title_case(f.name) }.join
254
+
255
+ renamed_with_same_wrappings(type_namer.generate_name_for(
256
+ :SubAggregations,
257
+ parent_agg_type: parent_aggregation_type(parent_doc_types),
258
+ field_path: field_part
259
+ ))
260
+ end
261
+
262
+ def as_parent_aggregation(parent_doc_types:)
263
+ schema_def_state.type_ref(parent_aggregation_type(parent_doc_types))
264
+ end
265
+
266
+ # Here we iterate over our mapping and generate dynamic methods for each category.
267
+ STATIC_FORMAT_NAME_BY_CATEGORY.keys.each do |category|
268
+ define_method(:"as_#{category}") do
269
+ # @type self: TypeReference
270
+ as_static_derived_type(category)
271
+ end
272
+ end
273
+
274
+ def list_filter_input?
275
+ matches_format_of?(:list_filter_input)
276
+ end
277
+
278
+ def list_element_filter_input?
279
+ matches_format_of?(:list_element_filter_input)
280
+ end
281
+
282
+ # These methods are defined dynamically above:
283
+ # @dynamic as_aggregated_values
284
+ # @dynamic as_grouped_by
285
+ # @dynamic as_aggregation
286
+ # @dynamic as_connection
287
+ # @dynamic as_edge
288
+ # @dynamic as_fields_list_filter_input
289
+ # @dynamic as_filter_input
290
+ # @dynamic as_input_enum
291
+ # @dynamic as_list_element_filter_input, list_element_filter_input?
292
+ # @dynamic as_list_filter_input, list_filter_input?
293
+ # @dynamic as_sort_order
294
+
295
+ private
296
+
297
+ def after_initialize
298
+ Mixins::VerifiesGraphQLName.verify_name!(unwrapped_name)
299
+ end
300
+
301
+ def peel_json_schema_layers_once
302
+ if list?
303
+ return [[:array], unwrap_list] if non_null?
304
+ return [[:nullable, :array], unwrap_list]
305
+ end
306
+
307
+ return [[], unwrap_non_null] if non_null?
308
+ [[:nullable], self]
309
+ end
310
+
311
+ def matches_format_of?(category)
312
+ format_name = STATIC_FORMAT_NAME_BY_CATEGORY.fetch(category)
313
+ type_namer.matches_format?(name, format_name)
314
+ end
315
+
316
+ def parent_aggregation_type(parent_doc_types)
317
+ __skip__ = case parent_doc_types
318
+ in [single_parent_type]
319
+ type_namer.generate_name_for(:Aggregation, base: single_parent_type)
320
+ in [*parent_types, last_parent_type]
321
+ type_namer.generate_name_for(:SubAggregation, parent_types: parent_types.join, base: last_parent_type)
322
+ else
323
+ raise SchemaError, "Unexpected `parent_doc_types`: #{parent_doc_types.inspect}. `parent_doc_types` must not be empty."
324
+ end
325
+ end
326
+
327
+ def renamed_with_same_wrappings(new_name)
328
+ pre_wrappings, post_wrappings = name.split(GRAPHQL_NAME_WITHIN_LARGER_STRING_PATTERN)
329
+ schema_def_state.type_ref("#{pre_wrappings}#{new_name}#{post_wrappings}")
330
+ end
331
+
332
+ ENUM_FORMATS = TypeNamer::DEFINITE_ENUM_FORMATS
333
+ OBJECT_FORMATS = TypeNamer::DEFINITE_OBJECT_FORMATS
334
+
335
+ def schema_kind_implied_by_name
336
+ name = type_namer.revert_override_for(self.name)
337
+ return :enum if ENUM_FORMATS.any? { |f| type_namer.matches_format?(name, f) }
338
+ return :object if OBJECT_FORMATS.any? { |f| type_namer.matches_format?(name, f) }
339
+
340
+ if (as_output_enum_name = type_namer.extract_base_from(name, format: :InputEnum))
341
+ :enum if ENUM_FORMATS.any? { |f| type_namer.matches_format?(as_output_enum_name, f) }
342
+ end
343
+ end
344
+
345
+ def to_title_case(name)
346
+ CamelCaseConverter.normalize_case(name).sub(/\A(\w)/, &:upcase)
347
+ end
348
+
349
+ CamelCaseConverter = SchemaArtifacts::RuntimeMetadata::SchemaElementNamesDefinition::CamelCaseConverter
350
+ end
351
+ end
352
+ end
353
+ end