From 3dc873ff72245ed040b1b01d000dd1a661b83722 Mon Sep 17 00:00:00 2001 From: Josh Wilson Date: Sun, 31 May 2026 15:26:47 -0500 Subject: [PATCH 1/3] Migrate JSON ingestion specs into gem --- .../schema_artifact_manager_extension_spec.rb | 342 ++++++++++++++++++ .../spec/spec_helper.rb | 10 + .../spec/support/json_schema_matcher.rb | 0 .../spec/support/json_schema_matcher_spec.rb | 0 .../json_schema_with_metadata_spec.rb | 6 +- .../indexing/wrappers_spec.rb | 138 +++++++ .../json_schema_field_metadata_spec.rb | 6 +- .../json_schema_pruner_spec.rb | 4 +- .../schema_definition/json_schema_spec.rb | 2 +- .../scalar_type_extension_spec.rb | 102 ++++++ .../graphql_schema/define_schema_spec.rb | 25 ++ .../index_definitions_by_name_spec.rb | 44 +++ .../spec_support/enable_simplecov.rb | 3 - 13 files changed, 670 insertions(+), 12 deletions(-) create mode 100644 elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb create mode 100644 elasticgraph-json_ingestion/spec/spec_helper.rb rename {elasticgraph-schema_definition => elasticgraph-json_ingestion}/spec/support/json_schema_matcher.rb (100%) rename {elasticgraph-schema_definition => elasticgraph-json_ingestion}/spec/support/json_schema_matcher_spec.rb (100%) rename {elasticgraph-schema_definition/spec/unit/elastic_graph => elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion}/schema_definition/indexing/json_schema_with_metadata_spec.rb (99%) create mode 100644 elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb rename {elasticgraph-schema_definition/spec/unit/elastic_graph => elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion}/schema_definition/json_schema_field_metadata_spec.rb (95%) rename {elasticgraph-schema_definition/spec/unit/elastic_graph => elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion}/schema_definition/json_schema_pruner_spec.rb (97%) rename {elasticgraph-schema_definition/spec/unit/elastic_graph => elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion}/schema_definition/json_schema_spec.rb (99%) create mode 100644 elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb diff --git a/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb b/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb new file mode 100644 index 000000000..7b34bc2ce --- /dev/null +++ b/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb @@ -0,0 +1,342 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/constants" +require "elastic_graph/json_ingestion/schema_definition/api_extension" +require "elastic_graph/schema_definition/rake_tasks" +require "fileutils" +require "yaml" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + RSpec.describe SchemaArtifactManagerExtension, :in_temp_dir, :rake_task do + after do + Thread.current[:eg_schema_load_count] = nil + end + + it "dumps public JSON schemas and private versioned JSON schemas with ElasticGraph metadata" do + write_schema(json_schema_version: 1) + output = run_rake("schema_artifacts:dump") + + expect(output.lines).to include( + a_string_including("Dumped", JSON_SCHEMAS_FILE), + a_string_including("Dumped", versioned_json_schema_file(1)) + ) + + public_id_schema = read_yaml_artifact(JSON_SCHEMAS_FILE).dig("$defs", "Widget", "properties", "id") + versioned_id_schema = read_yaml_artifact(versioned_json_schema_file(1)).dig("$defs", "Widget", "properties", "id") + + expect(public_id_schema).to eq(json_schema_for_keyword_type("ID")) + expect(versioned_id_schema).to eq(json_schema_for_keyword_type("ID", { + "ElasticGraph" => { + "type" => "ID!", + "nameInIndex" => "id" + } + })) + + expect(run_rake("schema_artifacts:dump")).to include("is already up to date", JSON_SCHEMAS_FILE) + end + + it "requires JSON schema version bumps unless enforcement is disabled" do + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + + write_schema(json_schema_version: 2) + expect { + run_rake("schema_artifacts:dump") + }.to change { read_artifact(JSON_SCHEMAS_FILE) } + .from(a_string_including("\njson_schema_version: 1\n")) + .to(a_string_including("\njson_schema_version: 2\n")) + + write_schema(json_schema_version: 2, extra_widget_body: "t.field 'color', 'String!'") + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "A change has been attempted to `json_schemas.yaml`", + "`schema.json_schema_version 3`" + ).and matching(/line \d+ at `(\S*\/?)schema\.rb`/) + + write_schema( + json_schema_version: 2, + extra_widget_body: "t.field 'color', 'String!'", + enforce_json_schema_version: false + ) + + expect(run_rake("schema_artifacts:dump")).to include( + "WARNING: the `json_schemas.yaml` artifact is being updated without the `json_schema_version` being correspondingly incremented" + ) + end + + it "keeps field metadata up to date on every versioned JSON schema" do + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + + write_schema(json_schema_version: 2, extra_widget_body: "t.field 'color', 'String!'") + run_rake("schema_artifacts:dump") + + write_schema( + json_schema_version: 2, + name_field_suffix: ", name_in_index: 'name2'", + extra_widget_body: "t.field 'color', 'String!'" + ) + run_rake("schema_artifacts:dump") + + loaded_v1 = read_yaml_artifact(versioned_json_schema_file(1)) + loaded_v2 = read_yaml_artifact(versioned_json_schema_file(2)) + + expect(loaded_v1.dig("$defs", "Widget", "properties", "name")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "name2" + } + }) + ) + expect(loaded_v1.dig("$defs", "Widget", "properties", "color")).to eq(nil) + + expect(loaded_v2.dig("$defs", "Widget", "properties", "name")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "name2" + } + }) + ) + expect(loaded_v2.dig("$defs", "Widget", "properties", "color")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "color" + } + }) + ) + end + + it "gives clear errors for old schema versions with missing fields or types" do + write_schema(json_schema_version: 8) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 9, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema version 8) no longer exists", + "at this old version", + "delete its file from `json_schemas_by_version`" + ) + + write_schema(json_schema_version: 9) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 10, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema versions 8 and 9) no longer exists", + "at these old versions", + "delete their files from `json_schemas_by_version`" + ) + + write_schema(json_schema_version: 10) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 11, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema versions 8, 9, and 10) no longer exists" + ) + + write_schema(json_schema_version: 11, omit_widget_name_field: true, extra_widget_body: "t.field('full_name', 'String') { |f| f.renamed_from 'name' }") + run_rake("schema_artifacts:dump") + + delete_artifact(JSON_SCHEMAS_FILE) + write_schema(json_schema_version: 11, omit_widget_name_field: true, extra_widget_body: "t.deleted_field 'name'") + run_rake("schema_artifacts:dump") + + delete_artifacts + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 2, widget_type_name: "Widget2") + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget` type (which existed in JSON schema version 1) no longer exists", + "If the `Widget` type has been renamed" + ) + end + + it "reports deprecated schema element warnings, conflicts, and missing necessary fields" do + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + schema.deleted_type "SomeType" + + schema.object_type "Widget" do |t| + t.renamed_from "OldWidget" + t.deleted_field "old_name" + t.field "id", "ID!" + t.field "name", "String" do |f| + f.renamed_from "old_name" + end + t.index "widgets" + end + end + EOS + + expect(run_rake("schema_artifacts:dump")).to include( + "The schema definition has 4 unneeded reference(s)", + "`schema.deleted_type \"SomeType\"`", + "`type.renamed_from \"OldWidget\"`", + "`type.deleted_field \"old_name\"`", + "`field.renamed_from \"old_name\"`" + ) + + delete_artifacts + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + schema.deleted_type "Widget" + + schema.object_type "Widget" do |t| + t.field "id", "ID!" + t.index "widgets" + + t.field "token", "ID" do |f| + f.renamed_from "id" + end + t.deleted_field "id" + end + end + EOS + + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "The schema definition of `Widget` has conflicts", + "The schema definition of `Widget.id` has conflicts" + ) + + delete_artifacts + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + + schema.object_type "Embedded" do |t| + t.field "workspace_id", "ID" + t.field "created_at", "DateTime" + end + + schema.object_type "Widget" do |t| + t.field "id", "ID" + t.field "embedded", "Embedded" + t.index "widgets" do |i| + i.route_with "embedded.workspace_id" + i.rollover :yearly, "embedded.created_at" + end + end + end + EOS + + run_rake("schema_artifacts:dump") + + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 2 + + schema.object_type "Embedded" do |t| + t.field "workspace_id2", "ID", name_in_index: "workspace_id" + t.deleted_field "workspace_id" + + t.field "created_at2", "DateTime", name_in_index: "created_at" + t.deleted_field "created_at" + end + + schema.object_type "Widget" do |t| + t.field "id", "ID" + t.field "embedded", "Embedded" + t.index "widgets" do |i| + i.route_with "embedded.workspace_id2" + i.rollover :yearly, "embedded.created_at2" + end + end + end + EOS + + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "JSON schema version 1 has no field that maps to the routing field path of `Widget.embedded.workspace_id`", + "JSON schema version 1 has no field that maps to the rollover field path of `Widget.embedded.created_at`" + ) + end + + def write_schema( + json_schema_version:, + enforce_json_schema_version: true, + widget_type_name: "Widget", + name_field_suffix: "", + extra_widget_body: "", + omit_widget_name_field: false + ) + ::File.write("schema.rb", <<~EOS) + Thread.current[:eg_schema_load_count] = (Thread.current[:eg_schema_load_count] || 0) + 1 + raise "Schema file was loaded more than once!" if Thread.current[:eg_schema_load_count] > 1 + + ElasticGraph.define_schema do |schema| + schema.json_schema_version #{json_schema_version} + #{"schema.enforce_json_schema_version false" unless enforce_json_schema_version} + + schema.object_type "#{widget_type_name}" do |t| + t.field "id", "ID!" + #{%(t.field "name", "String!"#{name_field_suffix}) unless omit_widget_name_field} + #{extra_widget_body} + t.index "widgets" + end + end + EOS + end + + def run_rake(*args) + Thread.current[:eg_schema_load_count] = nil + + super(*args) do |output| + ::ElasticGraph::SchemaDefinition::RakeTasks.new( + schema_element_name_form: :snake_case, + index_document_sizes: true, + path_to_schema: "schema.rb", + schema_artifacts_directory: "config/schema/artifacts", + extension_modules: [APIExtension], + output: output + ) + end + end + + def read_artifact(*name_parts) + path = ::File.join("config", "schema", "artifacts", *name_parts) + ::File.exist?(path) && ::File.read(path) + end + + def read_yaml_artifact(*name_parts) + ::YAML.safe_load(read_artifact(*name_parts)) + end + + def delete_artifact(*name_parts) + ::File.delete(::File.join("config", "schema", "artifacts", *name_parts)) + end + + def delete_artifacts + ::FileUtils.rm_rf(::File.join("config", "schema", "artifacts")) + end + + def versioned_json_schema_file(version) + ::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{version}.yaml") + end + + def json_schema_for_keyword_type(type, extras = {}) + { + "allOf" => [ + {"$ref" => "#/$defs/#{type}"}, + {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH} + ] + }.merge(extras) + end + end + end + end +end diff --git a/elasticgraph-json_ingestion/spec/spec_helper.rb b/elasticgraph-json_ingestion/spec/spec_helper.rb new file mode 100644 index 000000000..138d7e9ca --- /dev/null +++ b/elasticgraph-json_ingestion/spec/spec_helper.rb @@ -0,0 +1,10 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +# This file contains RSpec configuration for `elasticgraph-json_ingestion`. +# It is loaded by the shared spec helper at `spec_support/spec_helper.rb`. diff --git a/elasticgraph-schema_definition/spec/support/json_schema_matcher.rb b/elasticgraph-json_ingestion/spec/support/json_schema_matcher.rb similarity index 100% rename from elasticgraph-schema_definition/spec/support/json_schema_matcher.rb rename to elasticgraph-json_ingestion/spec/support/json_schema_matcher.rb diff --git a/elasticgraph-schema_definition/spec/support/json_schema_matcher_spec.rb b/elasticgraph-json_ingestion/spec/support/json_schema_matcher_spec.rb similarity index 100% rename from elasticgraph-schema_definition/spec/support/json_schema_matcher_spec.rb rename to elasticgraph-json_ingestion/spec/support/json_schema_matcher_spec.rb diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb similarity index 99% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb index 2e87e57ce..021d71793 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb @@ -10,9 +10,9 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition module Indexing - ::RSpec.describe JSONIngestion::SchemaDefinition::Indexing::JSONSchemaWithMetadata do + ::RSpec.describe JSONSchemaWithMetadata do include_context "SchemaDefinitionHelpers" it "ignores derived indexed types that do not show up in the JSON schema" do @@ -1056,7 +1056,7 @@ def metadata_for(json_schema, type, field) def define_schema(&schema_definition) super( schema_element_name_form: "snake_case", - extension_modules: [JSONIngestion::SchemaDefinition::APIExtension], + extension_modules: [APIExtension], &schema_definition ) end diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb new file mode 100644 index 000000000..dd4ec1022 --- /dev/null +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb @@ -0,0 +1,138 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/json_ingestion/schema_definition/indexing/field" +require "elastic_graph/json_ingestion/schema_definition/indexing/field_reference" +require "elastic_graph/json_ingestion/schema_definition/indexing/field_type/object" +require "elastic_graph/spec_support/schema_definition_helpers" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + module Indexing + RSpec.describe "JSON schema indexing wrappers" do + include_context "SchemaDefinitionHelpers" + + it "compares field references using both the wrapped reference and JSON schema options" do + reference = widget_schema_field("name").to_indexing_field_reference + matching_reference = FieldReference.new( + reference.__getobj__, + json_schema_layers: reference.json_schema_layers, + json_schema_customizations: reference.json_schema_customizations, + doc_comment: reference.doc_comment + ) + different_reference = FieldReference.new( + reference.__getobj__, + json_schema_layers: reference.json_schema_layers, + json_schema_customizations: {maxLength: 10}, + doc_comment: reference.doc_comment + ) + + expect(reference).to eq(matching_reference) + expect(reference.eql?(matching_reference)).to eq(true) + expect(reference.hash).to eq(matching_reference.hash) + expect(reference).not_to eq(different_reference) + expect(reference == reference.__getobj__).to eq(true) + end + + it "returns nil when resolving a field reference whose type is unresolved" do + reference = FieldReference.new( + ::ElasticGraph::SchemaDefinition::Indexing::FieldReference.new( + name: "missing_type", + name_in_index: "missing_type", + type: unresolved_type_ref, + mapping_options: {}, + accuracy_confidence: nil, + source: nil, + runtime_field_script: nil + ), + json_schema_layers: [], + json_schema_customizations: {}, + doc_comment: nil + ) + + expect(reference.resolve).to eq(nil) + end + + it "compares fields using both the wrapped field and JSON schema options" do + field = widget_indexing_field("name") + matching_field = Field.new( + field.__getobj__, + json_schema_layers: field.json_schema_layers, + json_schema_customizations: field.json_schema_customizations, + doc_comment: field.doc_comment + ) + different_field = Field.new( + field.__getobj__, + json_schema_layers: field.json_schema_layers, + json_schema_customizations: {maxLength: 10}, + doc_comment: field.doc_comment + ) + + expect(field).to eq(matching_field) + expect(field.eql?(matching_field)).to eq(true) + expect(field.hash).to eq(matching_field.hash) + expect(field).not_to eq(different_field) + expect(field == field.__getobj__).to eq(true) + end + + it "compares object field types using both the wrapped field type and JSON schema options" do + object_field_type = widget_field_type + matching_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| + field_type.json_schema_options = object_field_type.json_schema_options + end + different_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| + field_type.json_schema_options = {type: "object"} + end + + expect(object_field_type).to eq(matching_object_field_type) + expect(object_field_type.eql?(matching_object_field_type)).to eq(true) + expect(object_field_type.hash).to eq(matching_object_field_type.hash) + expect(object_field_type).not_to eq(different_object_field_type) + expect(object_field_type == object_field_type.__getobj__).to eq(true) + end + + def widget_schema_field(name) + widget_type.indexing_fields_by_name_in_index.fetch(name) + end + + def widget_indexing_field(name) + widget_field_type.subfields.find { |field| field.name == name } + end + + def widget_field_type + widget_type.to_indexing_field_type + end + + def widget_type + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.object_type "Widget" do |type| + type.field "id", "ID!" + type.field "name", "String" do |field| + field.json_schema minLength: 1 + end + end + end.state.object_types_by_name.fetch("Widget") + end + + def unresolved_type_ref + Class.new do + def fully_unwrapped + self + end + + def resolved + nil + end + end.new + end + end + end + end + end +end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb similarity index 95% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb index 450ae447d..0ebfeafe0 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb @@ -10,7 +10,7 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition ::RSpec.describe "JSON schema field metadata generation" do include_context "SchemaDefinitionHelpers" @@ -143,13 +143,13 @@ def dump_metadata(&schema_definition) def define_schema(&schema_definition) super( schema_element_name_form: "snake_case", - extension_modules: [JSONIngestion::SchemaDefinition::APIExtension], + extension_modules: [APIExtension], &schema_definition ) end def field_meta_of(type, name_in_index) - JSONIngestion::SchemaDefinition::Indexing::JSONSchemaFieldMetadata.new(type: type, name_in_index: name_in_index) + Indexing::JSONSchemaFieldMetadata.new(type: type, name_in_index: name_in_index) end end end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb similarity index 97% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb index e9c01c9f2..c8327661f 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb @@ -11,8 +11,8 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition - RSpec.describe JSONIngestion::SchemaDefinition::JSONSchemaPruner do + module JSONIngestion::SchemaDefinition + RSpec.describe JSONSchemaPruner do include_context "SchemaDefinitionHelpers" describe ".prune" do diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb similarity index 99% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb index cdaa8ba7b..4390f9890 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb @@ -13,7 +13,7 @@ require "support/json_schema_matcher" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition ::RSpec.describe "JSON schema generation" do include_context "SchemaDefinitionHelpers" json_schema_id = {"allOf" => [{"$ref" => "#/$defs/ID"}, {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH}]} diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb new file mode 100644 index 000000000..5e3f16424 --- /dev/null +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb @@ -0,0 +1,102 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/constants" +require "elastic_graph/errors" +require "elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension" +require "elastic_graph/spec_support/schema_definition_helpers" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + module SchemaElements + RSpec.describe ScalarTypeExtension do + include_context "SchemaDefinitionHelpers" + + it "requires custom scalar types to declare their JSON schema representation" do + expect { + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.scalar_type "BigInt" do |type| + type.mapping type: "long" + end + end + }.to raise_error Errors::SchemaError, a_string_including("BigInt", "lacks `json_schema`") + end + + it "extends schema elements created without customization blocks" do + api = build_api + api.enum_type "EmptyEnum" + api.interface_type "EmptyInterface" + direct_type_with_subfields = api.factory.new_type_with_subfields( + :object, + "DirectObject", + wrapping_type: nil, + field_factory: api.factory.method(:new_field) + ) + + expect(api.state.enum_types_by_name.fetch("EmptyEnum").singleton_class.ancestors).to include(EnumTypeExtension) + expect(api.state.object_types_by_name.fetch("EmptyInterface").__getobj__.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) + expect(direct_type_with_subfields.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) + + expect { + build_api.scalar_type "BigInt" + }.to raise_error Errors::SchemaError, a_string_including("BigInt", "lacks `json_schema`") + end + + it "infers a numeric missing-value placeholder for JSON-safe unsigned_long scalars with custom coercion" do + grouping_missing_value_placeholder = grouping_missing_value_placeholder_for( + "unsigned_long", + type: "integer", + maximum: JSON_SAFE_LONG_MAX + ) do |type| + type.coerce_with "ExampleScalarCoercionAdapter", defined_at: scalar_coercion_adapter_path + end + + expect(grouping_missing_value_placeholder).to eq(MISSING_NUMERIC_PLACEHOLDER) + end + + it "does not infer a numeric missing-value placeholder for unsigned_long scalars outside the JSON-safe range" do + grouping_missing_value_placeholder = grouping_missing_value_placeholder_for( + "unsigned_long", + type: "integer", + maximum: JSON_SAFE_LONG_MAX + 1 + ) do |type| + type.coerce_with "ExampleScalarCoercionAdapter", defined_at: scalar_coercion_adapter_path + end + + expect(grouping_missing_value_placeholder).to eq(nil) + end + + def grouping_missing_value_placeholder_for(mapping_type, **json_schema_options) + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.scalar_type "CustomScalar" do |type| + type.mapping type: mapping_type + type.json_schema(**json_schema_options) + yield type + end + end.runtime_metadata.scalar_types_by_name.fetch("CustomScalar").grouping_missing_value_placeholder + end + + def scalar_coercion_adapter_path + ::File.join(CommonSpecHelpers::REPO_ROOT, "elasticgraph-schema_definition/spec/support/example_extensions/scalar_coercion_adapter") + end + + def build_api + schema_elements = ::ElasticGraph::SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") + ::ElasticGraph::SchemaDefinition::API.new( + schema_elements, + true, + extension_modules: [APIExtension], + output: log_device + ) + end + end + end + end + end +end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb index b490ca4b7..2bce32f36 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb @@ -99,6 +99,31 @@ module SchemaDefinition ) end + it "rejects type names reserved by schema definition extensions" do + expect { + define_schema do |schema| + schema.object_type EVENT_ENVELOPE_JSON_SCHEMA_NAME + end + }.to raise_error Errors::SchemaError, a_string_including( + "`#{EVENT_ENVELOPE_JSON_SCHEMA_NAME}` cannot be used as a schema type", + "reserved name" + ) + end + + it "allows test schemas to skip JSON schema version setup" do + result = define_schema(json_schema_version: nil) do |schema| + schema.object_type("Widget") do |t| + t.field "id", "ID" + end + end + + expect(type_def_from(result, "Widget")).to eq(<<~EOS.strip) + type Widget { + id: ID + } + EOS + end + it "produces the same GraphQL output, regardless of the order the types are defined in" do object_type_definitions = { "Component" => lambda do |t| diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb index 1835088ba..3be37da65 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb @@ -41,6 +41,28 @@ module SchemaDefinition end end + it "rejects `route_with` fields that resolve to a non-leaf type" do + expect { + index_definition_metadata_for("widgets") do |i| + i.route_with "nested_fields_gql" + end + }.to raise_error Errors::SchemaError, a_string_including( + "shard routing field `MyType.nested_fields_gql", + "is not a leaf field" + ) + end + + it "includes nested field-path guidance for unresolved nested `route_with` fields" do + expect { + index_definition_metadata_for("widgets") do |i| + i.route_with "nested_fields_gql.missing" + end + }.to raise_error Errors::SchemaError, a_string_including( + "Field `MyType.nested_fields_gql.missing` cannot be resolved", + "Verify that all fields and types referenced by `nested_fields_gql.missing` are defined." + ) + end + it "defaults `route_with` to `id` because that's the default routing the datastore uses" do components = index_definition_metadata_for("components") expect(components.route_with).to eq "id" @@ -59,6 +81,28 @@ module SchemaDefinition expect(components.rollover).to eq nil end + it "rejects rollover fields that are not date or datetime fields" do + expect { + index_definition_metadata_for("widgets") do |i| + i.rollover :monthly, "group_id" + end + }.to raise_error Errors::SchemaError, a_string_including( + "rollover field `MyType.group_id", + "is not a `Date` or `DateTime` field" + ) + end + + it "rejects rollover fields that are lists" do + expect { + index_definition_metadata_for("widgets", on_my_type: ->(t) { t.field "timestamps", "[DateTime!]!" }) do |i| + i.rollover :monthly, "timestamps" + end + }.to raise_error Errors::SchemaError, a_string_including( + "rollover field `MyType.timestamps", + "is a list field" + ) + end + it "dumps the `rollover` timestamp field's `name_in_index`" do widgets = index_definition_metadata_for("widgets") do |i| i.rollover :monthly, "created_at_gql" diff --git a/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb b/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb index a9769863a..0d87095f3 100644 --- a/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb +++ b/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb @@ -103,9 +103,6 @@ def wait_for_other_processes # status if we're not running it's test suite. add_filter "/elasticgraph-local/" unless spec_files_to_run.any? { |f| f.include?("/elasticgraph-local/") } - # The JSON ingestion gem is being introduced by extracting implementation first and moving its tests later. - add_filter "/elasticgraph-json_ingestion/" - # This version file is loaded from our gemspecs, which can get loaded by bundler before we get here. # SimpleCov is only able to track coverage of files loaded after it starts, so we need to filter them out if # their constant is already defined. They don't contain any branching statements or anything so it's ok to From d80ab114e09645404238ad0d17421f663191ec75 Mon Sep 17 00:00:00 2001 From: Josh Wilson Date: Tue, 9 Jun 2026 13:51:05 -0500 Subject: [PATCH 2/3] Address review feedback on JSON ingestion spec migration - Trim the JSON-schema-versioning scenarios from the core `rake_tasks_spec` that are now covered by `schema_artifact_manager_extension_spec` in `elasticgraph-json_ingestion`, and remove the helpers that only those scenarios used. - Replace the core coverage those scenarios provided with focused unit specs: the `deleted_type`/`deleted_field`/`renamed_from` registration DSL, `FieldPath#fully_qualified_path_in_index`, and the test-support behavior when a schema sets its own JSON schema version. - Make `doc_comment` load-bearing in the wrapper equality specs by adding cases that differ only in `doc_comment`. - Assert on observable behavior instead of `singleton_class.ancestors` in the blockless-element extension spec. - Explain why `unresolved_type_ref` uses a stand-in. --- .../indexing/wrappers_spec.rb | 22 + .../scalar_type_extension_spec.rb | 17 +- .../schema_definition/rake_tasks_spec.rb | 516 +----------------- .../graphql_schema/define_schema_spec.rb | 17 + .../deprecated_element_spec.rb | 53 ++ .../schema_elements/field_path_spec.rb | 29 +- 6 files changed, 135 insertions(+), 519 deletions(-) create mode 100644 elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/deprecated_element_spec.rb diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb index dd4ec1022..21ad0cd5a 100644 --- a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb @@ -32,11 +32,18 @@ module Indexing json_schema_customizations: {maxLength: 10}, doc_comment: reference.doc_comment ) + different_doc_reference = FieldReference.new( + reference.__getobj__, + json_schema_layers: reference.json_schema_layers, + json_schema_customizations: reference.json_schema_customizations, + doc_comment: "alternate docs" + ) expect(reference).to eq(matching_reference) expect(reference.eql?(matching_reference)).to eq(true) expect(reference.hash).to eq(matching_reference.hash) expect(reference).not_to eq(different_reference) + expect(reference).not_to eq(different_doc_reference) expect(reference == reference.__getobj__).to eq(true) end @@ -73,11 +80,18 @@ module Indexing json_schema_customizations: {maxLength: 10}, doc_comment: field.doc_comment ) + different_doc_field = Field.new( + field.__getobj__, + json_schema_layers: field.json_schema_layers, + json_schema_customizations: field.json_schema_customizations, + doc_comment: "alternate docs" + ) expect(field).to eq(matching_field) expect(field.eql?(matching_field)).to eq(true) expect(field.hash).to eq(matching_field.hash) expect(field).not_to eq(different_field) + expect(field).not_to eq(different_doc_field) expect(field == field.__getobj__).to eq(true) end @@ -89,11 +103,16 @@ module Indexing different_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| field_type.json_schema_options = {type: "object"} end + different_doc_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| + field_type.json_schema_options = object_field_type.json_schema_options + field_type.doc_comment = "alternate docs" + end expect(object_field_type).to eq(matching_object_field_type) expect(object_field_type.eql?(matching_object_field_type)).to eq(true) expect(object_field_type.hash).to eq(matching_object_field_type.hash) expect(object_field_type).not_to eq(different_object_field_type) + expect(object_field_type).not_to eq(different_doc_object_field_type) expect(object_field_type == object_field_type.__getobj__).to eq(true) end @@ -120,6 +139,9 @@ def widget_type end.state.object_types_by_name.fetch("Widget") end + # A minimal stand-in for a `SchemaElements::TypeReference` that never resolves. References to + # not-yet-defined types resolve to `nil` mid-definition; a stand-in lets us exercise that path + # directly instead of relying on the timing of a partially-defined schema. def unresolved_type_ref Class.new do def fully_unwrapped diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb index 5e3f16424..2cd4d2cc6 100644 --- a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb @@ -39,9 +39,20 @@ module SchemaElements field_factory: api.factory.method(:new_field) ) - expect(api.state.enum_types_by_name.fetch("EmptyEnum").singleton_class.ancestors).to include(EnumTypeExtension) - expect(api.state.object_types_by_name.fetch("EmptyInterface").__getobj__.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) - expect(direct_type_with_subfields.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) + # An enum's derived GraphQL types are built from a derived scalar twin, which can only be + # built if `EnumTypeExtension` configured the twin's `json_schema`; otherwise building it + # raises a "lacks `json_schema`" error. + expect { + api.state.enum_types_by_name.fetch("EmptyEnum").derived_graphql_types + }.not_to raise_error + + # `json_schema` is only available on types extended with `TypeWithSubfieldsExtension`. + interface_type = api.state.object_types_by_name.fetch("EmptyInterface") + interface_type.json_schema minProperties: 1 + expect(interface_type.json_schema_options).to eq({minProperties: 1}) + + direct_type_with_subfields.json_schema minProperties: 2 + expect(direct_type_with_subfields.json_schema_options).to eq({minProperties: 2}) expect { build_api.scalar_type "BigInt" diff --git a/elasticgraph-schema_definition/spec/integration/elastic_graph/schema_definition/rake_tasks_spec.rb b/elasticgraph-schema_definition/spec/integration/elastic_graph/schema_definition/rake_tasks_spec.rb index edb0a967e..ef2703cde 100644 --- a/elasticgraph-schema_definition/spec/integration/elastic_graph/schema_definition/rake_tasks_spec.rb +++ b/elasticgraph-schema_definition/spec/integration/elastic_graph/schema_definition/rake_tasks_spec.rb @@ -124,116 +124,6 @@ module SchemaDefinition expect_missing_versioned_json_schema_artifact "v2.yaml" end - it "throws an error if the json_schemas artifact is (attempted to be) changed without json_schema_version being bumped" do - write_elastic_graph_schema_def_code(json_schema_version: 1) - expect_all_artifacts_out_of_date_because_they_havent_been_dumped - - # Should succeed, for first artifact. - expect { - output = run_rake("schema_artifacts:dump") - expect(output.lines).to include( - a_string_including("Dumped", JSON_SCHEMAS_FILE), - a_string_including("Dumped", versioned_json_schema_file(1)) - ) - }.to change { read_artifact(JSON_SCHEMAS_FILE) } - .from(a_falsy_value) - .to(a_string_including("\njson_schema_version: 1\n")) - .and change { read_artifact(versioned_json_schema_file(1)) } - .from(a_falsy_value) - .to(a_string_including("\njson_schema_version: 1\n")) - - expect_up_to_date_artifacts - - write_elastic_graph_schema_def_code(json_schema_version: 2) - - # Should succeed, it is ok to update the schema_version without underlying contents changing. - expect { - output = run_rake("schema_artifacts:dump") - expect(output.lines).to include( - a_string_including("Dumped", JSON_SCHEMAS_FILE), - a_string_including("Dumped", versioned_json_schema_file(2)) - ) - }.to change { read_artifact(JSON_SCHEMAS_FILE) } - .from(a_string_including("\njson_schema_version: 1")) - .to(a_string_including("\njson_schema_version: 2")) - .and change { read_artifact(versioned_json_schema_file(2)) } - .from(a_falsy_value) - .to(a_string_including("\njson_schema_version: 2\n")) - - write_elastic_graph_schema_def_code(component_suffix: "2", json_schema_version: 2, component_extras: "t.renamed_from 'Component'") - expect_out_of_date_artifacts - - expect { - run_rake("schema_artifacts:dump") - }.to abort_with a_string_including( - "A change has been attempted to `json_schemas.yaml`", - "`schema.json_schema_version 3`" - ).and matching(json_schema_version_setter_location_regex) - - # Still out of date. - expect_out_of_date_artifacts - - # Decreasing the json_schema_version should also result in a failure. - write_elastic_graph_schema_def_code(component_suffix: "2", json_schema_version: 1, component_extras: "t.renamed_from 'Component'") - expect_out_of_date_artifacts - - expect { - run_rake("schema_artifacts:dump") - }.to abort_with a_string_including( - "A change has been attempted to `json_schemas.yaml`", - "`schema.json_schema_version 3`" - ).and matching(json_schema_version_setter_location_regex) - - write_elastic_graph_schema_def_code(component_suffix: "2", json_schema_version: 3, component_extras: "t.renamed_from 'Component'") - - # Now dump should succeed, as schema_version has been bumped. - expect { - output = run_rake("schema_artifacts:dump") - expect(output.lines).to include( - a_string_including("Dumped", JSON_SCHEMAS_FILE), - a_string_including("Dumped", versioned_json_schema_file(3)) - ) - }.to change { read_artifact(JSON_SCHEMAS_FILE) } - .from(a_string_including("\njson_schema_version: 2")) - .to(a_string_including("\njson_schema_version: 3")) - .and change { read_artifact(versioned_json_schema_file(3)) } - .from(a_falsy_value) - .to(a_string_including("\njson_schema_version: 3\n")) - - # Should be able to run `schema_artifacts:dump` idempotently. - output = run_rake("schema_artifacts:dump") - expect(output.lines).to include( - a_string_including("is already up to date", JSON_SCHEMAS_FILE), - a_string_including("is already up to date", versioned_json_schema_file(3)) - ) - - write_elastic_graph_schema_def_code(component_suffix: "3", json_schema_version: 3, component_extras: "t.renamed_from 'Component'") - expect_out_of_date_artifacts - - expect { - run_rake("schema_artifacts:dump") - }.to abort_with a_string_including( - "A change has been attempted to `json_schemas.yaml`", - "`schema.json_schema_version 4`" - ).and matching(json_schema_version_setter_location_regex) - - write_elastic_graph_schema_def_code( - component_suffix: "3", - json_schema_version: 3, - component_extras: "t.renamed_from 'Component'", - enforce_json_schema_version: false - ) - - expect { - output = run_rake("schema_artifacts:dump") - expect(output.lines).to include( - a_string_including("Dumped", JSON_SCHEMAS_FILE), - a_string_including("Dumped", versioned_json_schema_file(3)) - ) - }.to change { read_artifact(JSON_SCHEMAS_FILE) } - .and change { read_artifact(versioned_json_schema_file(3)) } - end - it "allows the derived GraphQL type name formats to be customized" do # Disable documentation comment wrapping that the GraphQL gem does when formatting an SDL string. # We need to disable it because the customized derived type formats used below change the length @@ -399,383 +289,6 @@ module SchemaDefinition expect(filtered_types).to match_array(allowed_list) end - it "dumps the ElasticGraph JSON schema metadata only on the internal versioned JSON schema, omitting it from the public copy" do - write_elastic_graph_schema_def_code(json_schema_version: 1) - run_rake("schema_artifacts:dump") - - expect(::YAML.safe_load(read_artifact(JSON_SCHEMAS_FILE)).dig("$defs", "Component", "properties", "id")).to eq( - json_schema_for_keyword_type("ID") - ) - - expect(::YAML.safe_load(read_artifact(versioned_json_schema_file(1))).dig("$defs", "Component", "properties", "id")).to eq( - json_schema_for_keyword_type("ID", { - "ElasticGraph" => { - "type" => "ID!", - "nameInIndex" => "id" - } - }) - ) - end - - it "keeps the ElasticGraph JSON schema metadata up-to-date on all versioned JSON schemas" do - write_elastic_graph_schema_def_code(json_schema_version: 1) - run_rake("schema_artifacts:dump") - - expect(::YAML.safe_load(read_artifact(versioned_json_schema_file(1))).dig("$defs", "Component", "properties", "name")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "name" - } - }) - ) - - # Here we add a new field `another: String` - write_elastic_graph_schema_def_code(json_schema_version: 2, component_name_extras: "\nt.field 'another', 'String!'") - run_rake("schema_artifacts:dump") - - # It's not added to v1.yaml... - loaded_v1 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(1))) - expect(loaded_v1.dig("$defs", "Component", "properties", "name")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "name" - } - }) - ) - expect(loaded_v1.dig("$defs", "Component", "properties", "another")).to eq(nil) - - # ..but is added to v2.yaml. - loaded_v2 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(2))) - expect(loaded_v2.dig("$defs", "Component", "properties", "name")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "name" - } - }) - ) - expect(loaded_v2.dig("$defs", "Component", "properties", "another")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "another" - } - }) - ) - - # Here we keep the newly added field `another: String` and also change the `name_in_index` of `name`. - write_elastic_graph_schema_def_code(json_schema_version: 2, component_name_extras: ", name_in_index: 'name2'\nt.field 'another', 'String!'") - run_rake("schema_artifacts:dump") - - # The `name_in_index` for `name` should be changed to `name2` in the v1 schema... - loaded_v1 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(1))) - expect(loaded_v1.dig("$defs", "Component", "properties", "name")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "name2" - } - }) - ) - expect(loaded_v1.dig("$defs", "Component", "properties", "another")).to eq(nil) - - # ...and in the v1 schema. - loaded_v2 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(2))) - expect(loaded_v2.dig("$defs", "Component", "properties", "name")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "name2" - } - }) - ) - expect(loaded_v2.dig("$defs", "Component", "properties", "another")).to eq( - json_schema_for_keyword_type("String", { - "ElasticGraph" => { - "type" => "String!", - "nameInIndex" => "another" - } - }) - ) - - # Here we add a different new field (`ordinal: Int!`), without bumping the version (and using `enforce_json_schema_version: false` - # to not have to bump the version)... - write_elastic_graph_schema_def_code( - json_schema_version: 2, - component_name_extras: "\nt.field 'ordinal', 'Int!'", - enforce_json_schema_version: false - ) - run_rake("schema_artifacts:dump") - - # It should not be added to the v1 schema... - loaded_v1 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(1))) - expect(loaded_v1.dig("$defs", "Component", "properties", "ordinal")).to eq(nil) - - # ...but it should be added to the v2 schema. - loaded_v2 = ::YAML.safe_load(read_artifact(versioned_json_schema_file(2))) - expect(loaded_v2.dig("$defs", "Component", "properties", "ordinal")).to eq({ - "$ref" => "#/$defs/Int", - "ElasticGraph" => {"type" => "Int!", "nameInIndex" => "ordinal"} - }) - end - - it "gives the user a clear error when there is ambiguity about what to do with a renamed or deleted field" do - # Verify the error message with 1 old JSON schema version (v8). - write_elastic_graph_schema_def_code(json_schema_version: 8) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 9, omit_component_name_field: true) - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component.name` field (which existed in JSON schema version 8) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this field's data when ingesting events at this old version. - To continue, do one of the following: - - 1. If the `Component.name` field has been renamed, indicate this by calling `field.renamed_from "name"` on the renamed field. - 2. If the `Component.name` field has been dropped, indicate this by calling `type.deleted_field "name"` on the `Component` type. - 3. Alternately, if no publishers or in-flight events use JSON schema version 8, delete its file from `json_schemas_by_version`, and no further changes are required. - EOS - - # Verify the error message with 2 old JSON schema version (v8 and v9). - # The grammar/phrasing is adjusted slightly (e.g. "versions 8 and 9"). - write_elastic_graph_schema_def_code(json_schema_version: 9) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 10, omit_component_name_field: true) - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component.name` field (which existed in JSON schema versions 8 and 9) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this field's data when ingesting events at these old versions. - To continue, do one of the following: - - 1. If the `Component.name` field has been renamed, indicate this by calling `field.renamed_from "name"` on the renamed field. - 2. If the `Component.name` field has been dropped, indicate this by calling `type.deleted_field "name"` on the `Component` type. - 3. Alternately, if no publishers or in-flight events use JSON schema versions 8 or 9, delete their files from `json_schemas_by_version`, and no further changes are required. - EOS - - # Verify the error message with 3 old JSON schema version (v8, v9, and v10). - # The grammar/phrasing is adjusted slightly (e.g. "versions 8, 9, and 10"). - write_elastic_graph_schema_def_code(json_schema_version: 10) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 11, omit_component_name_field: true) - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component.name` field (which existed in JSON schema versions 8, 9, and 10) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this field's data when ingesting events at these old versions. - To continue, do one of the following: - - 1. If the `Component.name` field has been renamed, indicate this by calling `field.renamed_from "name"` on the renamed field. - 2. If the `Component.name` field has been dropped, indicate this by calling `type.deleted_field "name"` on the `Component` type. - 3. Alternately, if no publishers or in-flight events use JSON schema versions 8, 9, or 10, delete their files from `json_schemas_by_version`, and no further changes are required. - EOS - - # Demonstrate that these issues can be solved by each of the 3 options given. - # First, demonstrate indicating the field has been renamed. - write_elastic_graph_schema_def_code(json_schema_version: 11, omit_component_name_field: true, component_extras: "t.field('full_name', 'String') { |f| f.renamed_from 'name' }") - run_rake("schema_artifacts:dump") - delete_artifact(JSON_SCHEMAS_FILE) # so it doesn't force us to increment the version to 5 - - # Next, demonstrate indicating the field has been deleted. - write_elastic_graph_schema_def_code(json_schema_version: 11, omit_component_name_field: true, component_extras: "t.deleted_field 'name'") - run_rake("schema_artifacts:dump") - - # Finally, demonstrate deleting the old JSON schema version artifacts - delete_artifact(versioned_json_schema_file(8)) - delete_artifact(versioned_json_schema_file(9)) - delete_artifact(versioned_json_schema_file(10)) - write_elastic_graph_schema_def_code(json_schema_version: 11, omit_component_name_field: true) - run_rake("schema_artifacts:dump") - end - - it "gives the user a clear error when there is ambiguity about what to do with a renamed or deleted type" do - # Verify the error message with 1 old JSON schema version (v1). - write_elastic_graph_schema_def_code(json_schema_version: 1) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 2, component_suffix: "2") - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component` type (which existed in JSON schema version 1) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this type's data when ingesting events at this old version. - To continue, do one of the following: - - 1. If the `Component` type has been renamed, indicate this by calling `type.renamed_from "Component"` on the renamed type. - 2. If the `Component` type has been dropped, indicate this by calling `schema.deleted_type "Component"` on the schema. - 3. Alternately, if no publishers or in-flight events use JSON schema version 1, delete its file from `json_schemas_by_version`, and no further changes are required. - EOS - - # Verify the error message with 2 old JSON schema version (v1 and v2). - # The grammar/phrasing is adjusted slightly (e.g. "versions 1 and 2"). - write_elastic_graph_schema_def_code(json_schema_version: 2) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 3, component_suffix: "2") - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component` type (which existed in JSON schema versions 1 and 2) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this type's data when ingesting events at these old versions. - To continue, do one of the following: - - 1. If the `Component` type has been renamed, indicate this by calling `type.renamed_from "Component"` on the renamed type. - 2. If the `Component` type has been dropped, indicate this by calling `schema.deleted_type "Component"` on the schema. - 3. Alternately, if no publishers or in-flight events use JSON schema versions 1 or 2, delete their files from `json_schemas_by_version`, and no further changes are required. - EOS - - # Verify the error message with 3 old JSON schema version (v1, v2, and v3). - # The grammar/phrasing is adjusted slightly (e.g. "versions 1, 2, and 3"). - write_elastic_graph_schema_def_code(json_schema_version: 3) - run_rake("schema_artifacts:dump") - write_elastic_graph_schema_def_code(json_schema_version: 4, component_suffix: "2") - expect { run_rake("schema_artifacts:dump") }.to abort_with <<~EOS - The `Component` type (which existed in JSON schema versions 1, 2, and 3) no longer exists in the current schema definition. - ElasticGraph cannot guess what it should do with this type's data when ingesting events at these old versions. - To continue, do one of the following: - - 1. If the `Component` type has been renamed, indicate this by calling `type.renamed_from "Component"` on the renamed type. - 2. If the `Component` type has been dropped, indicate this by calling `schema.deleted_type "Component"` on the schema. - 3. Alternately, if no publishers or in-flight events use JSON schema versions 1, 2, or 3, delete their files from `json_schemas_by_version`, and no further changes are required. - EOS - - # Demonstrate that these issues can be solved by each of the 3 options given. - # First, demonstrate indicating the type has been renamed. - write_elastic_graph_schema_def_code(json_schema_version: 4, component_suffix: "2", component_extras: "t.renamed_from 'Component'") - run_rake("schema_artifacts:dump") - delete_artifact(JSON_SCHEMAS_FILE) # so it doesn't force us to increment the version to 5 - - # Next, demonstrate indicating the type has been deleted. - write_elastic_graph_schema_def_code(json_schema_version: 4, component_suffix: "2", component_extras: "schema.deleted_type 'Component'") - run_rake("schema_artifacts:dump") - - # Finally, demonstrate deleting the old JSON schema version artifacts - delete_artifact(versioned_json_schema_file(1)) - delete_artifact(versioned_json_schema_file(2)) - delete_artifact(versioned_json_schema_file(3)) - write_elastic_graph_schema_def_code(json_schema_version: 4, component_suffix: "2") - run_rake("schema_artifacts:dump") - end - - it "warns if there are `deleted_*` or `renamed_from` calls that are not needed so the user knows they can remove them" do - ::File.write("schema.rb", <<~EOS) - ElasticGraph.define_schema do |schema| - schema.json_schema_version 1 - schema.deleted_type "SomeType" - - schema.object_type "Widget" do |t| - t.renamed_from "Widget2" - t.deleted_field "name" - t.field "description", "String" do |f| - f.renamed_from "old_description" - end - t.renamed_from "Widget3" - - t.field "id", "ID" - t.index "widgets" - end - end - EOS - - output = run_rake("schema_artifacts:dump") - expect(output.split("\n").first(9).join("\n")).to eq(<<~EOS.strip) - The schema definition has 5 unneeded reference(s) to deprecated schema elements. These can all be safely deleted: - - 1. `schema.deleted_type "SomeType"` at schema.rb:3 - 2. `type.renamed_from "Widget2"` at schema.rb:6 - 3. `type.deleted_field "name"` at schema.rb:7 - 4. `field.renamed_from "old_description"` at schema.rb:9 - 5. `type.renamed_from "Widget3"` at schema.rb:11 - - Dumped schema artifact to `config/schema/artifacts/datastore_config.yaml`. - EOS - end - - it "gives a clear error if excess `deleted_*` or `renamed_from` calls create a conflict" do - ::File.write("schema.rb", <<~EOS) - ElasticGraph.define_schema do |schema| - schema.json_schema_version 1 - schema.deleted_type "Widget" - - schema.object_type "Widget" do |t| - t.field "id", "ID" - t.index "widgets" - - t.field "token", "ID" do |f| - f.renamed_from "id" - end - t.deleted_field "id" - end - end - EOS - - expect { - run_rake("schema_artifacts:dump") - }.to abort_with(<<~EOS) - The schema definition of `Widget` has conflicts. To resolve the conflict, remove the unneeded definitions from the following: - - 1. `schema.deleted_type "Widget"` at schema.rb:3 - - - The schema definition of `Widget.id` has conflicts. To resolve the conflict, remove the unneeded definitions from the following: - - 1. `field.renamed_from "id"` at schema.rb:10 - 2. `type.deleted_field "id"` at schema.rb:12 - EOS - end - - it "does not allow a routing or rollover field to be deleted since we cannot index documents without values for those fields" do - ::File.write("schema.rb", <<~EOS) - ElasticGraph.define_schema do |schema| - schema.json_schema_version 1 - - schema.object_type "Embedded" do |t| - t.field "workspace_id", "ID" - t.field "created_at", "DateTime" - end - - schema.object_type "Widget" do |t| - t.field "id", "ID" - t.field "embedded", "Embedded" - t.index "widgets" do |i| - i.route_with "embedded.workspace_id" - i.rollover :yearly, "embedded.created_at" - end - end - end - EOS - - run_rake("schema_artifacts:dump") - - ::File.write("schema.rb", <<~EOS) - ElasticGraph.define_schema do |schema| - schema.json_schema_version 2 - - schema.object_type "Embedded" do |t| - t.field "workspace_id2", "ID", name_in_index: "workspace_id" - t.deleted_field "workspace_id" - - t.field "created_at2", "DateTime", name_in_index: "created_at" - t.deleted_field "created_at" - end - - schema.object_type "Widget" do |t| - t.field "id", "ID" - t.field "embedded", "Embedded" - t.index "widgets" do |i| - i.route_with "embedded.workspace_id2" - i.rollover :yearly, "embedded.created_at2" - end - end - end - EOS - - expect { run_rake("schema_artifacts:dump") }.to abort_with(<<~EOS) - JSON schema version 1 has no field that maps to the routing field path of `Widget.embedded.workspace_id`. - Since the field path is required for routing, ElasticGraph cannot ingest events that lack it. To continue, do one of the following: - - 1. If the `Widget.embedded.workspace_id` field has been renamed, indicate this by calling `field.renamed_from "workspace_id"` on the renamed field rather than using `deleted_field`. - 2. Alternately, if no publishers or in-flight events use JSON schema version 1, delete its file from `json_schemas_by_version`, and no further changes are required. - - - JSON schema version 1 has no field that maps to the rollover field path of `Widget.embedded.created_at`. - Since the field path is required for rollover, ElasticGraph cannot ingest events that lack it. To continue, do one of the following: - - 1. If the `Widget.embedded.created_at` field has been renamed, indicate this by calling `field.renamed_from "created_at"` on the renamed field rather than using `deleted_field`. - 2. Alternately, if no publishers or in-flight events use JSON schema version 1, delete its file from `json_schemas_by_version`, and no further changes are required. - EOS - end - it "does not change the formatting of the dumped artifacts in unexpected ways" do config_dir = File.join(CommonSpecHelpers::REPO_ROOT, "config") run_rake("schema_artifacts:dump", path_to_schema: File.join(config_dir, "schema.rb"), include_extension_module: false) @@ -906,16 +419,7 @@ def expect_successful_run_of(*shell_commands) }.to output(/Your Gemfile lists/).to_stderr_from_any_process end - let(:json_schema_version_setter_location_regex) do - # In `write_elastic_graph_schema_def_code` `json_schema_version` is called on the 7th line of - # the file written to `schema.rb` (after the 5-line double-load guard). See below. - # - # Note: on Ruby 3.3, the path here winds up being slightly different; instead of just `schema.rb` it is something like: - # `../d20240216-23551-cvdjzo/schema.rb`. I think it's related to the temp directory we run these specs within. - /line 7 at `(\S*\/?)schema\.rb`/ - end - - def write_elastic_graph_schema_def_code(json_schema_version:, component_suffix: "", extra_sdl: "", component_name_extras: "", component_extras: "", omit_component_name_field: false, enforce_json_schema_version: true) + def write_elastic_graph_schema_def_code(json_schema_version:, component_suffix: "", extra_sdl: "", component_extras: "") code = <<~EOS Thread.current[:eg_schema_load_count] = (Thread.current[:eg_schema_load_count] || 0) + 1 if Thread.current[:eg_schema_load_count] > 1 @@ -924,7 +428,6 @@ def write_elastic_graph_schema_def_code(json_schema_version:, component_suffix: ElasticGraph.define_schema do |schema| schema.json_schema_version #{json_schema_version} - #{"schema.enforce_json_schema_version false" unless enforce_json_schema_version} schema.enum_type "Size" do |t| t.values "SMALL", "MEDIUM", "LAGE" end @@ -955,7 +458,7 @@ def write_elastic_graph_schema_def_code(json_schema_version:, component_suffix: schema.object_type "Component#{component_suffix}" do |t| t.field "id", "ID!" - #{%(t.field "name", "String!"#{component_name_extras}) unless omit_component_name_field} + t.field "name", "String!" t.field "designer_id", "ID" t.index "components#{component_suffix}", number_of_shards: 5 @@ -1053,12 +556,6 @@ def expect_out_of_date_artifacts_with_details(example_diff, test_color: false) } end - def expect_out_of_date_artifacts - expect { - run_rake("schema_artifacts:check") - }.to abort_with a_string_including("out of date", DATASTORE_CONFIG_FILE, JSON_SCHEMAS_FILE) - end - def read_artifact(name) path = File.join("config", "schema", "artifacts", name) File.exist?(path) && File.read(path) @@ -1110,15 +607,6 @@ def as_active_instance end end - def json_schema_for_keyword_type(type, extras = {}) - { - "allOf" => [ - {"$ref" => "#/$defs/#{type}"}, - {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH} - ] - }.merge(extras) - end - def enum_types_in_dumped_graphql_schema ::GraphQL::Schema.from_definition(read_artifact(GRAPHQL_SCHEMA_FILE)).types.filter_map do |name, type| name if type.kind.enum? && !name.start_with?("__") diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb index 2bce32f36..b230f0543 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb @@ -124,6 +124,23 @@ module SchemaDefinition EOS end + it "allows test schemas to set the JSON schema version themselves" do + # If the test support logic re-set the version it would fail with a "can only be set once" error. + result = define_schema do |schema| + schema.json_schema_version 7 + + schema.object_type("Widget") do |t| + t.field "id", "ID" + end + end + + expect(type_def_from(result, "Widget")).to eq(<<~EOS.strip) + type Widget { + id: ID + } + EOS + end + it "produces the same GraphQL output, regardless of the order the types are defined in" do object_type_definitions = { "Component" => lambda do |t| diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/deprecated_element_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/deprecated_element_spec.rb new file mode 100644 index 000000000..418279df4 --- /dev/null +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/deprecated_element_spec.rb @@ -0,0 +1,53 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/spec_support/schema_definition_helpers" + +module ElasticGraph + module SchemaDefinition + module SchemaElements + RSpec.describe DeprecatedElement do + include_context "SchemaDefinitionHelpers" + + it "records `deleted_type`, `deleted_field`, and `renamed_from` calls so that schema artifact tooling can consume them" do + state = define_schema(schema_element_name_form: "snake_case", extension_modules: []) do |schema| + schema.deleted_type "OldType" + + schema.object_type "Widget" do |t| + t.renamed_from "OldWidget" + t.deleted_field "legacy_name" + + t.field "id", "ID!" + t.field "name", "String" do |f| + f.renamed_from "old_name" + end + end + end.state + + expect(state.deleted_types_by_old_name.keys).to eq ["OldType"] + expect(state.renamed_types_by_old_name.keys).to eq ["OldWidget"] + expect(state.deleted_fields_by_type_name_and_old_field_name.fetch("Widget").keys).to eq ["legacy_name"] + expect(state.renamed_fields_by_type_name_and_old_field_name.fetch("Widget").keys).to eq ["old_name"] + + expect(state.deleted_types_by_old_name.fetch("OldType").description).to match( + /\A`schema\.deleted_type "OldType"` at .+:\d+\z/ + ) + expect(state.renamed_types_by_old_name.fetch("OldWidget").description).to match( + /\A`type\.renamed_from "OldWidget"` at .+:\d+\z/ + ) + expect(state.deleted_fields_by_type_name_and_old_field_name.fetch("Widget").fetch("legacy_name").description).to match( + /\A`type\.deleted_field "legacy_name"` at .+:\d+\z/ + ) + expect(state.renamed_fields_by_type_name_and_old_field_name.fetch("Widget").fetch("old_name").description).to match( + /\A`field\.renamed_from "old_name"` at .+:\d+\z/ + ) + end + end + end + end +end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/field_path_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/field_path_spec.rb index 7e3cb11dc..4c38deba3 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/field_path_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/schema_elements/field_path_spec.rb @@ -15,8 +15,7 @@ module SchemaElements class FieldPath RSpec.describe Resolver do it "can only be created after the user definition is complete, to avoid problems" do - schema_elements = SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") - api = API.new(schema_elements, true) + api = build_api expect { Resolver.new(api.state) @@ -28,6 +27,32 @@ class FieldPath expect(Resolver.new(api.state)).to be_a Resolver end + + it "describes resolved paths using the parent type name and the `name_in_index` of each part" do + api = build_api + + api.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "cost", "Money" + t.index "widgets" + end + + api.object_type "Money" do |t| + t.field "amount", "Int", name_in_index: "amount_in_index" + end + + api.results # signals the definition is complete + + widget_type = api.state.object_types_by_name.fetch("Widget") + path = Resolver.new(api.state).resolve_public_path(widget_type, "cost.amount") { |field| true } + + expect(path.fully_qualified_path_in_index).to eq("Widget.cost.amount_in_index") + end + + def build_api + schema_elements = SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") + API.new(schema_elements, true) + end end end end From 9c881ac14f9c615366d7c127ce8073d2002a0015 Mon Sep 17 00:00:00 2001 From: Josh Wilson Date: Tue, 9 Jun 2026 14:59:58 -0500 Subject: [PATCH 3/3] Drop unneeded namespace prefix in scalar type extension spec --- .../schema_elements/scalar_type_extension_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb index 2cd4d2cc6..ed1614d94 100644 --- a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb @@ -98,7 +98,7 @@ def scalar_coercion_adapter_path end def build_api - schema_elements = ::ElasticGraph::SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") + schema_elements = SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") ::ElasticGraph::SchemaDefinition::API.new( schema_elements, true,