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,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