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,482 @@
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/schema_definition/json_schema_pruner"
12
+ require "elastic_graph/support/memoizable_data"
13
+ require "fileutils"
14
+ require "graphql"
15
+ require "tempfile"
16
+ require "yaml"
17
+
18
+ module ElasticGraph
19
+ module SchemaDefinition
20
+ # Manages schema artifacts. Note: not tested directly. Instead, the `RakeTasks` tests drive this class.
21
+ #
22
+ # Note that we use `abort` instead of `raise` here for exceptions that require the user to perform an action
23
+ # to resolve. The output from `abort` is cleaner (no stack trace, etc) which improves the signal-to-noise
24
+ # ratio for the user to (hopefully) make it easier to understand what to do, without needing to wade through
25
+ # extra output.
26
+ #
27
+ # @private
28
+ class SchemaArtifactManager
29
+ # @dynamic schema_definition_results
30
+ attr_reader :schema_definition_results
31
+
32
+ def initialize(schema_definition_results:, schema_artifacts_directory:, enforce_json_schema_version:, output:, max_diff_lines: 50)
33
+ @schema_definition_results = schema_definition_results
34
+ @schema_artifacts_directory = schema_artifacts_directory
35
+ @enforce_json_schema_version = enforce_json_schema_version
36
+ @output = output
37
+ @max_diff_lines = max_diff_lines
38
+
39
+ @json_schemas_artifact = new_yaml_artifact(
40
+ JSON_SCHEMAS_FILE,
41
+ JSONSchemaPruner.prune(schema_definition_results.current_public_json_schema),
42
+ extra_comment_lines: [
43
+ "This is the \"public\" JSON schema file and is intended to be provided to publishers so that",
44
+ "they can perform code generation and event validation."
45
+ ]
46
+ )
47
+
48
+ # Here we round-trip the SDL string through the GraphQL gem's formatting logic. This provides
49
+ # nice, consistent formatting (alphabetical order, consistent spacing, etc) and also prunes out
50
+ # any "orphaned" schema types (that is, types that are defined but never referenced).
51
+ # We also prepend a line break so there's a blank line between the comment block and the
52
+ # schema elements.
53
+ graphql_schema = ::GraphQL::Schema.from_definition(schema_definition_results.graphql_schema_string).to_definition.chomp
54
+
55
+ unversioned_artifacts = [
56
+ new_yaml_artifact(DATASTORE_CONFIG_FILE, schema_definition_results.datastore_config),
57
+ new_yaml_artifact(RUNTIME_METADATA_FILE, pruned_runtime_metadata(graphql_schema).to_dumpable_hash),
58
+ @json_schemas_artifact,
59
+ new_raw_artifact(GRAPHQL_SCHEMA_FILE, "\n" + graphql_schema)
60
+ ]
61
+
62
+ versioned_artifacts = build_desired_versioned_json_schemas(@json_schemas_artifact.desired_contents).values.map do |versioned_schema|
63
+ new_versioned_json_schema_artifact(versioned_schema)
64
+ end
65
+
66
+ @artifacts = (unversioned_artifacts + versioned_artifacts).sort_by(&:file_name)
67
+ notify_about_unused_type_name_overrides
68
+ notify_about_unused_enum_value_overrides
69
+ end
70
+
71
+ # Dumps all the schema artifacts to disk.
72
+ def dump_artifacts
73
+ check_if_needs_json_schema_version_bump do |recommended_json_schema_version|
74
+ if @enforce_json_schema_version
75
+ # @type var setter_location: ::Thread::Backtrace::Location
76
+ # We use `_ =` because while `json_schema_version_setter_location` can be nil,
77
+ # it'll never be nil if we get here and we want the type to be non-nilable.
78
+ setter_location = _ = schema_definition_results.json_schema_version_setter_location
79
+ setter_location_path = ::Pathname.new(setter_location.absolute_path.to_s).relative_path_from(::Dir.pwd)
80
+
81
+ abort "A change has been attempted to `json_schemas.yaml`, but the `json_schema_version` has not been correspondingly incremented. Please " \
82
+ "increase the schema's version, and then run the `schema_artifacts:dump` command again.\n\n" \
83
+ "To update the schema version to the expected version, change line #{setter_location.lineno} at `#{setter_location_path}` to:\n" \
84
+ " `schema.json_schema_version #{recommended_json_schema_version}`\n\n" \
85
+ "Alternately, pass `enforce_json_schema_version: false` to `ElasticGraph::SchemaDefinition::RakeTasks.new` to allow the JSON schemas " \
86
+ "file to change without requiring a version bump, but that is only recommended for non-production applications during initial schema prototyping."
87
+ else
88
+ @output.puts <<~EOS
89
+ WARNING: the `json_schemas.yaml` artifact is being updated without the `json_schema_version` being correspondingly incremented.
90
+ This is not recommended for production applications, but is currently allowed because you have set `enforce_json_schema_version: false`.
91
+ EOS
92
+ end
93
+ end
94
+
95
+ ::FileUtils.mkdir_p(@schema_artifacts_directory)
96
+ @artifacts.each { |artifact| artifact.dump(@output) }
97
+ end
98
+
99
+ # Checks that all schema artifacts are up-to-date, raising an exception if not.
100
+ def check_artifacts
101
+ out_of_date_artifacts = @artifacts.select(&:out_of_date?)
102
+
103
+ if out_of_date_artifacts.empty?
104
+ descriptions = @artifacts.map.with_index(1) { |art, i| "#{i}. #{art.file_name}" }
105
+ @output.puts <<~EOS
106
+ Your schema artifacts are all up to date:
107
+ #{descriptions.join("\n")}
108
+
109
+ EOS
110
+ else
111
+ abort artifacts_out_of_date_error(out_of_date_artifacts)
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def notify_about_unused_type_name_overrides
118
+ type_namer = @schema_definition_results.state.type_namer
119
+ return if (unused_overrides = type_namer.unused_name_overrides).empty?
120
+
121
+ suggester = ::DidYouMean::SpellChecker.new(dictionary: type_namer.used_names.to_a)
122
+ warnings = unused_overrides.map.with_index(1) do |(unused_name, _), index|
123
+ alternatives = suggester.correct(unused_name).map { |alt| "`#{alt}`" }
124
+ "#{index}. The type name override `#{unused_name}` does not match any type in your GraphQL schema and has been ignored." \
125
+ "#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"
126
+ end
127
+
128
+ @output.puts <<~EOS
129
+ WARNING: #{unused_overrides.size} of the `type_name_overrides` do not match any type(s) in your GraphQL schema:
130
+
131
+ #{warnings.join("\n")}
132
+ EOS
133
+ end
134
+
135
+ def notify_about_unused_enum_value_overrides
136
+ enum_value_namer = @schema_definition_results.state.enum_value_namer
137
+ return if (unused_overrides = enum_value_namer.unused_overrides).empty?
138
+
139
+ used_value_names_by_type_name = enum_value_namer.used_value_names_by_type_name
140
+ type_suggester = ::DidYouMean::SpellChecker.new(dictionary: used_value_names_by_type_name.keys)
141
+ index = 0
142
+ warnings = unused_overrides.flat_map do |type_name, overrides|
143
+ if used_value_names_by_type_name.key?(type_name)
144
+ value_suggester = ::DidYouMean::SpellChecker.new(dictionary: used_value_names_by_type_name.fetch(type_name))
145
+ overrides.map do |(value_name), _|
146
+ alternatives = value_suggester.correct(value_name).map { |alt| "`#{alt}`" }
147
+ "#{index += 1}. The enum value override `#{type_name}.#{value_name}` does not match any enum value in your GraphQL schema and has been ignored." \
148
+ "#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"
149
+ end
150
+ else
151
+ alternatives = type_suggester.correct(type_name).map { |alt| "`#{alt}`" }
152
+ ["#{index += 1}. `enum_value_overrides_by_type` has a `#{type_name}` key, which does not match any enum type in your GraphQL schema and has been ignored." \
153
+ "#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"]
154
+ end
155
+ end
156
+
157
+ @output.puts <<~EOS
158
+ WARNING: some of the `enum_value_overrides_by_type` do not match any type(s)/value(s) in your GraphQL schema:
159
+
160
+ #{warnings.join("\n")}
161
+ EOS
162
+ end
163
+
164
+ def build_desired_versioned_json_schemas(current_public_json_schema)
165
+ versioned_parsed_yamls = ::Dir.glob(::File.join(@schema_artifacts_directory, JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v*.yaml")).map do |file|
166
+ ::YAML.safe_load_file(file)
167
+ end + [current_public_json_schema]
168
+
169
+ results_by_json_schema_version = versioned_parsed_yamls.to_h do |parsed_yaml|
170
+ merged_schema = @schema_definition_results.merge_field_metadata_into_json_schema(parsed_yaml)
171
+ [merged_schema.json_schema_version, merged_schema]
172
+ end
173
+
174
+ report_json_schema_merge_errors(results_by_json_schema_version.values)
175
+ report_json_schema_merge_warnings
176
+
177
+ results_by_json_schema_version.transform_values(&:json_schema)
178
+ end
179
+
180
+ def report_json_schema_merge_errors(merged_results)
181
+ json_schema_versions_by_missing_field = ::Hash.new { |h, k| h[k] = [] }
182
+ json_schema_versions_by_missing_type = ::Hash.new { |h, k| h[k] = [] }
183
+ json_schema_versions_by_missing_necessary_field = ::Hash.new { |h, k| h[k] = [] }
184
+
185
+ merged_results.each do |result|
186
+ result.missing_fields.each do |field|
187
+ json_schema_versions_by_missing_field[field] << result.json_schema_version
188
+ end
189
+
190
+ result.missing_types.each do |type|
191
+ json_schema_versions_by_missing_type[type] << result.json_schema_version
192
+ end
193
+
194
+ result.missing_necessary_fields.each do |missing_necessary_field|
195
+ json_schema_versions_by_missing_necessary_field[missing_necessary_field] << result.json_schema_version
196
+ end
197
+ end
198
+
199
+ missing_field_errors = json_schema_versions_by_missing_field.map do |field, json_schema_versions|
200
+ missing_field_error_for(field, json_schema_versions)
201
+ end
202
+
203
+ missing_type_errors = json_schema_versions_by_missing_type.map do |type, json_schema_versions|
204
+ missing_type_error_for(type, json_schema_versions)
205
+ end
206
+
207
+ missing_necessary_field_errors = json_schema_versions_by_missing_necessary_field.map do |field, json_schema_versions|
208
+ missing_necessary_field_error_for(field, json_schema_versions)
209
+ end
210
+
211
+ definition_conflict_errors = merged_results
212
+ .flat_map { |result| result.definition_conflicts.to_a }
213
+ .group_by(&:name)
214
+ .map do |name, deprecated_elements|
215
+ <<~EOS
216
+ The schema definition of `#{name}` has conflicts. To resolve the conflict, remove the unneeded definitions from the following:
217
+
218
+ #{format_deprecated_elements(deprecated_elements)}
219
+ EOS
220
+ end
221
+
222
+ errors = missing_field_errors + missing_type_errors + missing_necessary_field_errors + definition_conflict_errors
223
+ return if errors.empty?
224
+
225
+ abort errors.join("\n\n")
226
+ end
227
+
228
+ def report_json_schema_merge_warnings
229
+ unused_elements = @schema_definition_results.unused_deprecated_elements
230
+ return if unused_elements.empty?
231
+
232
+ @output.puts <<~EOS
233
+ The schema definition has #{unused_elements.size} unneeded reference(s) to deprecated schema elements. These can all be safely deleted:
234
+
235
+ #{format_deprecated_elements(unused_elements)}
236
+
237
+ EOS
238
+ end
239
+
240
+ def format_deprecated_elements(deprecated_elements)
241
+ descriptions = deprecated_elements
242
+ .sort_by { |e| [e.defined_at.path, e.defined_at.lineno] }
243
+ .map(&:description)
244
+ .uniq
245
+
246
+ descriptions.each.with_index(1).map { |desc, idx| "#{idx}. #{desc}" }.join("\n")
247
+ end
248
+
249
+ def missing_field_error_for(qualified_field, json_schema_versions)
250
+ type, field = qualified_field.split(".")
251
+
252
+ <<~EOS
253
+ The `#{qualified_field}` field (which existed in #{describe_json_schema_versions(json_schema_versions, "and")}) no longer exists in the current schema definition.
254
+ ElasticGraph cannot guess what it should do with this field's data when ingesting events at #{old_versions(json_schema_versions)}.
255
+ To continue, do one of the following:
256
+
257
+ 1. If the `#{qualified_field}` field has been renamed, indicate this by calling `field.renamed_from "#{field}"` on the renamed field.
258
+ 2. If the `#{qualified_field}` field has been dropped, indicate this by calling `type.deleted_field "#{field}"` on the `#{type}` type.
259
+ 3. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
260
+ EOS
261
+ end
262
+
263
+ def missing_type_error_for(type, json_schema_versions)
264
+ <<~EOS
265
+ The `#{type}` type (which existed in #{describe_json_schema_versions(json_schema_versions, "and")}) no longer exists in the current schema definition.
266
+ ElasticGraph cannot guess what it should do with this type's data when ingesting events at #{old_versions(json_schema_versions)}.
267
+ To continue, do one of the following:
268
+
269
+ 1. If the `#{type}` type has been renamed, indicate this by calling `type.renamed_from "#{type}"` on the renamed type.
270
+ 2. If the `#{type}` field has been dropped, indicate this by calling `schema.deleted_type "#{type}"` on the schema.
271
+ 3. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
272
+ EOS
273
+ end
274
+
275
+ def missing_necessary_field_error_for(field, json_schema_versions)
276
+ path = field.fully_qualified_path.split(".").last
277
+ # :nocov: -- we only cover one side of this ternary.
278
+ has_or_have = (json_schema_versions.size == 1) ? "has" : "have"
279
+ # :nocov:
280
+
281
+ <<~EOS
282
+ #{describe_json_schema_versions(json_schema_versions, "and")} #{has_or_have} no field that maps to the #{field.field_type} field path of `#{field.fully_qualified_path}`.
283
+ Since the field path is required for #{field.field_type}, ElasticGraph cannot ingest events that lack it. To continue, do one of the following:
284
+
285
+ 1. If the `#{field.fully_qualified_path}` field has been renamed, indicate this by calling `field.renamed_from "#{path}"` on the renamed field rather than using `deleted_field`.
286
+ 2. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
287
+ EOS
288
+ end
289
+
290
+ def describe_json_schema_versions(json_schema_versions, conjunction)
291
+ json_schema_versions = json_schema_versions.sort
292
+
293
+ # Steep doesn't support pattern matching yet, so have to skip type checking here.
294
+ __skip__ = case json_schema_versions
295
+ in [single_version]
296
+ "JSON schema version #{single_version}"
297
+ in [version1, version2]
298
+ "JSON schema versions #{version1} #{conjunction} #{version2}"
299
+ else
300
+ *versions, last_version = json_schema_versions
301
+ "JSON schema versions #{versions.join(", ")}, #{conjunction} #{last_version}"
302
+ end
303
+ end
304
+
305
+ def old_versions(json_schema_versions)
306
+ return "this old version" if json_schema_versions.size == 1
307
+ "these old versions"
308
+ end
309
+
310
+ def files_noun_phrase(json_schema_versions)
311
+ return "its file" if json_schema_versions.size == 1
312
+ "their files"
313
+ end
314
+
315
+ def artifacts_out_of_date_error(out_of_date_artifacts)
316
+ # @type var diffs: ::Array[[SchemaArtifact[untyped], ::String]]
317
+ diffs = []
318
+
319
+ descriptions = out_of_date_artifacts.map.with_index(1) do |artifact, index|
320
+ reason =
321
+ if (diff = artifact.diff(color: @output.tty?))
322
+ description, diff = truncate_diff(diff, @max_diff_lines)
323
+ diffs << [artifact, diff]
324
+ "see [#{diffs.size}] below for the #{description}"
325
+ else
326
+ "file does not exist"
327
+ end
328
+
329
+ "#{index}. #{artifact.file_name} (#{reason})"
330
+ end
331
+
332
+ diffs = diffs.map.with_index(1) do |(artifact, diff), index|
333
+ <<~EOS
334
+ [#{index}] #{artifact.file_name} diff:
335
+ #{diff}
336
+ EOS
337
+ end
338
+
339
+ <<~EOS.strip
340
+ #{out_of_date_artifacts.size} schema artifact(s) are out of date. Run `rake schema_artifacts:dump` to update the following artifact(s):
341
+
342
+ #{descriptions.join("\n")}
343
+
344
+ #{diffs.join("\n")}
345
+ EOS
346
+ end
347
+
348
+ def truncate_diff(diff, lines)
349
+ diff_lines = diff.lines
350
+
351
+ if diff_lines.size <= lines
352
+ ["diff", diff]
353
+ else
354
+ truncated = diff_lines.first(lines).join
355
+ ["first #{lines} lines of the diff", truncated]
356
+ end
357
+ end
358
+
359
+ def new_yaml_artifact(file_name, desired_contents, extra_comment_lines: [])
360
+ SchemaArtifact.new(
361
+ ::File.join(@schema_artifacts_directory, file_name),
362
+ desired_contents,
363
+ ->(hash) { ::YAML.dump(hash) },
364
+ ->(string) { ::YAML.safe_load(string) },
365
+ extra_comment_lines
366
+ )
367
+ end
368
+
369
+ def new_versioned_json_schema_artifact(desired_contents)
370
+ # File name depends on the schema_version field in the json schema.
371
+ schema_version = desired_contents[JSON_SCHEMA_VERSION_KEY]
372
+
373
+ new_yaml_artifact(
374
+ ::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{schema_version}.yaml"),
375
+ desired_contents,
376
+ extra_comment_lines: [
377
+ "This JSON schema file contains internal ElasticGraph metadata and should be considered private.",
378
+ "The unversioned JSON schema file is public and intended to be provided to publishers."
379
+ ]
380
+ )
381
+ end
382
+
383
+ def new_raw_artifact(file_name, desired_contents)
384
+ SchemaArtifact.new(
385
+ ::File.join(@schema_artifacts_directory, file_name),
386
+ desired_contents,
387
+ _ = :itself.to_proc,
388
+ _ = :itself.to_proc,
389
+ []
390
+ )
391
+ end
392
+
393
+ def check_if_needs_json_schema_version_bump(&block)
394
+ if @json_schemas_artifact.out_of_date?
395
+ existing_schema_version = @json_schemas_artifact.existing_dumped_contents&.dig(JSON_SCHEMA_VERSION_KEY) || -1
396
+ desired_schema_version = @json_schemas_artifact.desired_contents[JSON_SCHEMA_VERSION_KEY]
397
+
398
+ if existing_schema_version >= desired_schema_version
399
+ yield existing_schema_version + 1
400
+ end
401
+ end
402
+ end
403
+
404
+ def pruned_runtime_metadata(graphql_schema_string)
405
+ schema = ::GraphQL::Schema.from_definition(graphql_schema_string)
406
+ runtime_meta = schema_definition_results.runtime_metadata
407
+
408
+ schema_type_names = schema.types.keys
409
+ pruned_enum_types = runtime_meta.enum_types_by_name.slice(*schema_type_names)
410
+ pruned_scalar_types = runtime_meta.scalar_types_by_name.slice(*schema_type_names)
411
+ pruned_object_types = runtime_meta.object_types_by_name.slice(*schema_type_names)
412
+
413
+ runtime_meta.with(
414
+ enum_types_by_name: pruned_enum_types,
415
+ scalar_types_by_name: pruned_scalar_types,
416
+ object_types_by_name: pruned_object_types
417
+ )
418
+ end
419
+ end
420
+
421
+ # @private
422
+ class SchemaArtifact < Support::MemoizableData.define(:file_name, :desired_contents, :dumper, :loader, :extra_comment_lines)
423
+ def dump(output)
424
+ if out_of_date?
425
+ dirname = File.dirname(file_name)
426
+ FileUtils.mkdir_p(dirname) # Create directory if needed.
427
+
428
+ ::File.write(file_name, dumped_contents)
429
+ output.puts "Dumped schema artifact to `#{file_name}`."
430
+ else
431
+ output.puts "`#{file_name}` is already up to date."
432
+ end
433
+ end
434
+
435
+ def out_of_date?
436
+ (_ = existing_dumped_contents) != desired_contents
437
+ end
438
+
439
+ def existing_dumped_contents
440
+ return nil unless exists?
441
+
442
+ # We drop the first 2 lines because it is the comment block containing dynamic elements.
443
+ file_contents = ::File.read(file_name).split("\n").drop(2).join("\n")
444
+ loader.call(file_contents)
445
+ end
446
+
447
+ def diff(color:)
448
+ return nil unless exists?
449
+
450
+ ::Tempfile.create do |f|
451
+ f.write(dumped_contents.chomp)
452
+ f.fsync
453
+
454
+ `git diff --no-index #{file_name} #{f.path}#{" --color" if color}`
455
+ .gsub(file_name, "existing_contents")
456
+ .gsub(f.path, "/updated_contents")
457
+ end
458
+ end
459
+
460
+ private
461
+
462
+ def exists?
463
+ return !!@exists if defined?(@exists)
464
+ @exists = ::File.exist?(file_name)
465
+ end
466
+
467
+ def dumped_contents
468
+ @dumped_contents ||= "#{comment_preamble}\n#{dumper.call(desired_contents)}"
469
+ end
470
+
471
+ def comment_preamble
472
+ lines = [
473
+ "Generated by `rake schema_artifacts:dump`.",
474
+ "DO NOT EDIT BY HAND. Any edits will be lost the next time the rake task is run."
475
+ ]
476
+
477
+ lines = extra_comment_lines + [""] + lines unless extra_comment_lines.empty?
478
+ lines.map { |line| "# #{line}".strip }.join("\n")
479
+ end
480
+ end
481
+ end
482
+ end
@@ -0,0 +1,56 @@
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/schema_definition/mixins/has_directives"
10
+ require "elastic_graph/schema_definition/mixins/has_documentation"
11
+ require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
12
+ require "elastic_graph/schema_definition/mixins/supports_default_value"
13
+ require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
14
+
15
+ module ElasticGraph
16
+ module SchemaDefinition
17
+ # Namespace for classes which represent GraphQL schema elements.
18
+ module SchemaElements
19
+ # Represents a [GraphQL argument](https://spec.graphql.org/October2021/#sec-Language.Arguments).
20
+ #
21
+ # @!attribute [r] schema_def_state
22
+ # @return [State] state of the schema
23
+ # @!attribute [r] parent_field
24
+ # @return [Field] field which has this argument
25
+ # @!attribute [r] name
26
+ # @return [String] name of the argument
27
+ # @!attribute [r] original_value_type
28
+ # @return [TypeReference] type of the argument, as originally provided
29
+ # @see #value_type
30
+ class Argument < Struct.new(:schema_def_state, :parent_field, :name, :original_value_type)
31
+ prepend Mixins::VerifiesGraphQLName
32
+ prepend Mixins::SupportsDefaultValue
33
+ include Mixins::HasDocumentation
34
+ include Mixins::HasDirectives
35
+ include Mixins::HasReadableToSAndInspect.new { |a| "#{a.parent_field.parent_type.name}.#{a.parent_field.name}(#{a.name}: #{a.value_type})" }
36
+
37
+ # @return [String] GraphQL SDL form of the argument
38
+ def to_sdl
39
+ "#{formatted_documentation}#{name}: #{value_type}#{default_value_sdl}#{directives_sdl(prefix_with: " ")}"
40
+ end
41
+
42
+ # When the argument type is an enum, and we're configured with different naming for input vs output enums,
43
+ # we need to convert the value type to its input form. Note that this intentionally happens lazily (rather than
44
+ # doing this when `Argument` is instantiated), because the referenced type need not exist when the argument
45
+ # is defined, and we may not be able to figure out if it's an enum until the type has been defined. So, we
46
+ # apply this lazily.
47
+ #
48
+ # @return [TypeReference] the type of the argument
49
+ # @see #original_value_type
50
+ def value_type
51
+ original_value_type.to_final_form(as_input: true)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end