elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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