diff --git a/elasticgraph-protobuf/README.md b/elasticgraph-protobuf/README.md index 3d32c16c4..6465f796e 100644 --- a/elasticgraph-protobuf/README.md +++ b/elasticgraph-protobuf/README.md @@ -162,6 +162,36 @@ end When a mapping exists for an enum, `elasticgraph-protobuf` uses the mapped proto enum(s) as the source of enum values (respecting `exclusions`, `expected_extras`, and `name_transform`). +### Referencing Existing Protobuf Types + +For enums that exactly match a canonical proto enum, you can import and reference +the existing proto type instead of generating a duplicate local enum: + +```ruby +# in config/schema/protobuf.rb + +ElasticGraph.define_schema do |schema| + if defined?(Squareup::Connect::V2::Resources::Card::Type) + schema.proto_enum_mappings( + "CardType" => { + Squareup::Connect::V2::Resources::Card::Type => {} + } + ) + + schema.proto_external_types( + "CardType" => { + proto: "squareup.connect.v2.resources.Card.Type", + import: "squareup/connect/v2/resources/card.proto" + } + ) + end +end +``` + +External type references currently support enums only. The matching +`proto_enum_mappings` entry must have exactly one source and no transform options; +otherwise the enum stays generated locally so value curation remains explicit. + ### Stable Field Numbers `schema_artifacts:dump` automatically reads and writes `proto_field_numbers.yaml` diff --git a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/api_extension.rb b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/api_extension.rb index 76cac0902..b61bdf635 100644 --- a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/api_extension.rb +++ b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/api_extension.rb @@ -106,6 +106,26 @@ def proto_enum_mappings(proto_enums_by_graphql_enum) nil end + # Registers GraphQL types that should be referenced from existing proto files instead of + # generated locally in `schema.proto`. + # + # @param proto_external_types [Hash] map of GraphQL type name to `proto` and `import` values + # @return [void] + # + # @example Reference an external enum type + # ElasticGraph.define_schema do |schema| + # schema.proto_external_types( + # "CardType" => { + # proto: "squareup.connect.v2.resources.Card.Type", + # import: "squareup/connect/v2/resources/card.proto" + # } + # ) + # end + def proto_external_types(proto_external_types) + protobuf_state.proto_external_types = proto_external_types + nil + end + # Configures proto field-number mappings directly from a hash. # Useful for tests and advanced use cases where mappings are sourced outside artifacts. # When artifacts are dumped, mappings from the existing `proto_field_numbers.yaml` artifact diff --git a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/identifier.rb b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/identifier.rb index 28d4e3d59..c50f58134 100644 --- a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/identifier.rb +++ b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/identifier.rb @@ -51,6 +51,15 @@ def self.enum_value_name(name) escape_keyword(name.to_s) end + # Builds a reference to an externally-defined protobuf type. Unlike generated local + # identifiers, this preserves dotted fully-qualified names verbatim. + # + # @param name [#to_s] + # @return [String] + def self.external_type_name(name) + name.to_s + end + # Escapes protobuf reserved keywords by suffixing them with an underscore. # # @param identifier [String] diff --git a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/results_extension.rb b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/results_extension.rb index 43281f78b..57aecc12a 100644 --- a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/results_extension.rb +++ b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/results_extension.rb @@ -52,6 +52,7 @@ def protobuf_schema_generator state: state, package_name: state.proto_schema_package_name, proto_enums_by_graphql_enum: state.proto_enums_by_graphql_enum, + proto_external_types: state.proto_external_types, proto_field_number_mappings: state.proto_field_number_mappings, syntax: state.proto_schema_syntax, headers: state.proto_schema_headers diff --git a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/schema.rb b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/schema.rb index 50c41bd59..79a106da8 100644 --- a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/schema.rb +++ b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/schema.rb @@ -60,6 +60,13 @@ class Schema # @!attribute [r] name_in_index # @return [String] FieldNumberMapping = ::Data.define(:field_number, :name_in_index) + # Internal representation of an externally-defined protobuf type. + # + # @!attribute [r] fqn + # @return [String] + # @!attribute [r] import + # @return [String] + ExternalTypeDefinition = ::Data.define(:fqn, :import) # Protobuf syntaxes this generator can emit. SUPPORTED_SYNTAXES = %w[proto2 proto3].freeze @@ -67,6 +74,7 @@ class Schema # @param state [ElasticGraph::SchemaDefinition::State] # @param package_name [String] # @param proto_enums_by_graphql_enum [Hash] + # @param proto_external_types [Hash] # @param proto_field_number_mappings [Hash] # @param syntax [Symbol, String] `:proto3` (default) or `:proto2`; validated by {APIExtension#proto_schema_artifacts} # @param headers [Array] file-level header lines (e.g. `option` declarations) rendered verbatim @@ -74,6 +82,7 @@ def initialize( state:, package_name:, proto_enums_by_graphql_enum:, + proto_external_types: {}, proto_field_number_mappings: {}, syntax: :proto3, headers: [] @@ -83,7 +92,10 @@ def initialize( @state = state @package_name = Identifier.package_name(package_name) @proto_enums_by_graphql_enum = normalize_proto_enum_mappings(proto_enums_by_graphql_enum) + @proto_external_types_by_type_name = normalize_proto_external_types(proto_external_types) @proto_field_number_mappings_by_message = normalize_proto_field_number_mappings(proto_field_number_mappings) + @imports = ::Set.new + @registered_external_type_names = ::Set.new @message_definitions_by_name = {} @enum_definitions_by_name = {} @generated_message_definitions_by_name = {} @@ -105,8 +117,9 @@ def to_proto %(syntax = "#{@syntax}";), "package #{@package_name};", *render_headers, + *render_imports, render_definitions - ] + ].reject(&:empty?) sections.join("\n\n") + "\n" end @@ -146,6 +159,11 @@ def indexed_types # Registers the type's proto definition (if it needs one) and returns its proto field type name. def register_type(type) + if type.respond_to?(:name) && (external_type = @proto_external_types_by_type_name[type.name.to_s]) + register_external_type(type, external_type) + return external_type.fqn + end + case type when SchemaElements::EnumTypeExtension register_enum(type) @@ -161,6 +179,55 @@ def register_type(type) type.to_proto_field_type end + def register_external_type(type, external_type) + type_name = type.name.to_s + + case type + when SchemaElements::EnumTypeExtension + unless @registered_external_type_names.include?(type_name) + validate_external_enum_type(type) + @registered_external_type_names << type_name + end + + @imports << external_type.import + else + raise Errors::SchemaError, "External proto type `#{type.name}` cannot be referenced yet. " \ + "Only enum types are supported by `proto_external_types` in this release." + end + end + + def validate_external_enum_type(enum_type) + enum_type_name = enum_type.name.to_s + mapping_entries = @proto_enums_by_graphql_enum[enum_type_name] + if mapping_entries.nil? || mapping_entries.empty? + raise Errors::SchemaError, "External proto enum `#{enum_type_name}` must also configure " \ + "`proto_enum_mappings` with exactly one untransformed source so its values can be verified." + end + + unless mapping_entries.size == 1 + raise Errors::SchemaError, "External proto enum `#{enum_type_name}` must use exactly one " \ + "`proto_enum_mappings` source; multi-source enum mappings cannot be safely referenced externally." + end + + proto_type, options = mapping_entries.first + options_are_empty = options.nil? || (options.is_a?(Hash) && options.empty?) + unless options_are_empty + raise Errors::SchemaError, "External proto enum `#{enum_type_name}` must use an empty " \ + "`proto_enum_mappings` options hash; transformed, excluded, or extra values must stay generated locally." + end + + proto_value_names = enum_value_names_from_proto_mapping( + enum_type_name: enum_type_name, + proto_type: proto_type, + options: {} + ).uniq.sort + eg_value_names = enum_type.values_by_name.keys.map(&:to_s).uniq.sort + return if proto_value_names == eg_value_names + + raise Errors::SchemaError, "External proto enum `#{enum_type_name}` values do not match the ElasticGraph enum values. " \ + "External values: #{proto_value_names.join(", ")}. ElasticGraph values: #{eg_value_names.join(", ")}." + end + def register_message(type) message_name = Identifier.message_name(type.name) check_message_name_collision(message_name, type.name) @@ -428,6 +495,10 @@ def name_taken?(name) @enum_definitions_by_name.key?(name) end + def render_imports + @imports.sort.map { |import| "import \"#{import}\";" } + end + # Renders the custom header lines as a single contiguous section (so they are not # blank-line separated). Returns `[]` when no headers were configured. def render_headers @@ -543,6 +614,46 @@ def normalize_proto_enum_mappings(raw_mappings) normalized end + def normalize_proto_external_types(raw_mappings) + normalized = {} # : ::Hash[::String, ExternalTypeDefinition] + return normalized if raw_mappings.nil? + + unless raw_mappings.is_a?(Hash) + raise Errors::SchemaError, "External proto type mappings must be a Hash, got: #{raw_mappings.class}." + end + + raw_mappings.each do |type_name, mapping| + unless mapping.is_a?(Hash) + raise Errors::SchemaError, "External proto type mapping for `#{type_name}` must be a Hash." + end + + proto_type_name = fetch_external_type_mapping_value(type_name, mapping, :proto) + import = fetch_external_type_mapping_value(type_name, mapping, :import) + + normalized[type_name.to_s] = ExternalTypeDefinition.new( + fqn: Identifier.external_type_name(proto_type_name), + import: import + ) + end + + normalized + end + + def fetch_external_type_mapping_value(type_name, mapping, key) + value = + if mapping.key?(key) + mapping.fetch(key) + elsif mapping.key?(key.to_s) + mapping.fetch(key.to_s) + end + + if value.is_a?(String) && !value.empty? + value + else + raise Errors::SchemaError, "External proto type mapping for `#{type_name}` must include a non-empty `#{key}` String." + end + end + def normalize_proto_field_number_mappings(raw_mappings) return {} if raw_mappings.nil? unless raw_mappings.is_a?(Hash) diff --git a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/state_extension.rb b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/state_extension.rb index 96f3e3fc0..3eb207b3b 100644 --- a/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/state_extension.rb +++ b/elasticgraph-protobuf/lib/elastic_graph/protobuf/schema_definition/state_extension.rb @@ -15,15 +15,17 @@ module SchemaDefinition module StateExtension # @dynamic proto_schema_package_name, proto_schema_package_name= # @dynamic proto_enums_by_graphql_enum, proto_enums_by_graphql_enum= + # @dynamic proto_external_types, proto_external_types= # @dynamic proto_field_number_mappings, proto_field_number_mappings= # @dynamic proto_schema_syntax, proto_schema_syntax= # @dynamic proto_schema_headers, proto_schema_headers= - attr_accessor :proto_schema_package_name, :proto_enums_by_graphql_enum, + attr_accessor :proto_schema_package_name, :proto_enums_by_graphql_enum, :proto_external_types, :proto_field_number_mappings, :proto_schema_syntax, :proto_schema_headers def self.extended(state) state.proto_schema_package_name = "elasticgraph" state.proto_enums_by_graphql_enum = {} + state.proto_external_types = {} state.proto_field_number_mappings = {} state.proto_schema_syntax = :proto3 state.proto_schema_headers = [] diff --git a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/api_extension.rbs b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/api_extension.rbs index 77fc192df..71b1d12aa 100644 --- a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/api_extension.rbs +++ b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/api_extension.rbs @@ -7,6 +7,7 @@ module ElasticGraph def self.extended: (::ElasticGraph::SchemaDefinition::API & APIExtension) -> void def proto_schema_artifacts: (?package_name: ::String, ?syntax: ::Symbol, ?headers: ::Array[::String]) -> void def proto_enum_mappings: (untyped) -> void + def proto_external_types: (untyped) -> void def configure_proto_field_number_mappings: (untyped) -> void private diff --git a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/identifier.rbs b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/identifier.rbs index 0a19bd32d..c8eeedbcb 100644 --- a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/identifier.rbs +++ b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/identifier.rbs @@ -9,6 +9,7 @@ module ElasticGraph def self.enum_name: (::String) -> ::String def self.field_name: (::String) -> ::String def self.enum_value_name: (::String) -> ::String + def self.external_type_name: (::String) -> ::String def self.escape_keyword: (::String) -> ::String end end diff --git a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/schema.rbs b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/schema.rbs index ffd4a7ed4..764509e7c 100644 --- a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/schema.rbs +++ b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/schema.rbs @@ -51,6 +51,13 @@ module ElasticGraph def self.new: (field_number: ::Integer, name_in_index: ::String) -> instance end + class ExternalTypeDefinition + attr_reader fqn: ::String + attr_reader import: ::String + + def self.new: (fqn: ::String, import: ::String) -> instance + end + type fieldNumberMappingsByFieldName = ::Hash[::String, FieldNumberMapping] SUPPORTED_SYNTAXES: ::Array[::String] @@ -60,7 +67,10 @@ module ElasticGraph @state: ::ElasticGraph::SchemaDefinition::State @package_name: ::String @proto_enums_by_graphql_enum: ::Hash[::String, untyped] + @proto_external_types_by_type_name: ::Hash[::String, ExternalTypeDefinition] @proto_field_number_mappings_by_message: ::Hash[::String, fieldNumberMappingsByFieldName] + @imports: ::Set[::String] + @registered_external_type_names: ::Set[::String] @message_definitions_by_name: ::Hash[::String, MessageDefinition] @enum_definitions_by_name: ::Hash[::String, EnumDefinition] @generated_message_definitions_by_name: ::Hash[::String, MessageDefinition] @@ -73,6 +83,7 @@ module ElasticGraph state: ::ElasticGraph::SchemaDefinition::State, package_name: ::String, proto_enums_by_graphql_enum: untyped, + ?proto_external_types: untyped, ?proto_field_number_mappings: untyped, ?syntax: (::Symbol | ::String), ?headers: ::Array[::String] @@ -85,6 +96,8 @@ module ElasticGraph def indexed_types: () -> ::Array[untyped] def register_type: (untyped type) -> ::String + def register_external_type: (untyped type, ExternalTypeDefinition) -> void + def validate_external_enum_type: (untyped enum_type) -> void def register_message: (untyped type) -> void def register_enum: (untyped enum_type) -> void def enum_value_names_for: (untyped enum_type) -> ::Array[::String] @@ -121,6 +134,7 @@ module ElasticGraph ) -> ::String def unique_generated_message_name: (::String) -> ::String def name_taken?: (::String) -> bool + def render_imports: () -> ::Array[::String] def render_headers: () -> ::Array[::String] def render_definitions: () -> ::String def proto_enum_value_name: (::String, ::String) -> ::String @@ -135,6 +149,8 @@ module ElasticGraph def check_message_name_collision: (::String, ::String) -> void def check_enum_name_collision: (::String, ::String) -> void def normalize_proto_enum_mappings: (untyped) -> ::Hash[::String, untyped] + def normalize_proto_external_types: (untyped) -> ::Hash[::String, ExternalTypeDefinition] + def fetch_external_type_mapping_value: (::String | ::Symbol, ::Hash[untyped, untyped], ::Symbol) -> ::String def normalize_proto_field_number_mappings: (untyped) -> ::Hash[::String, fieldNumberMappingsByFieldName] def normalize_field_number_mapping_entry: (::String, ::String, untyped) -> [::Integer, ::String] def renamed_public_field_names_by_type_name: () -> ::Hash[::String, ::Hash[::String, ::Array[::String]]] diff --git a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/state_extension.rbs b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/state_extension.rbs index a1349ed46..29642503f 100644 --- a/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/state_extension.rbs +++ b/elasticgraph-protobuf/sig/elastic_graph/protobuf/schema_definition/state_extension.rbs @@ -4,6 +4,7 @@ module ElasticGraph module StateExtension: ::ElasticGraph::SchemaDefinition::State attr_accessor proto_schema_package_name: ::String attr_accessor proto_enums_by_graphql_enum: untyped + attr_accessor proto_external_types: untyped attr_accessor proto_field_number_mappings: untyped attr_accessor proto_schema_syntax: (::Symbol | ::String) attr_accessor proto_schema_headers: ::Array[::String] diff --git a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/identifier_spec.rb b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/identifier_spec.rb index fb87afe97..e1381fad2 100644 --- a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/identifier_spec.rb +++ b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/identifier_spec.rb @@ -27,6 +27,11 @@ module SchemaDefinition expect(Identifier.field_name("string")).to eq("string_") expect(Identifier.enum_value_name("stream")).to eq("stream_") end + + it "preserves external dotted type names" do + expect(Identifier.external_type_name("squareup.connect.v2.resources.Card.Type")) + .to eq("squareup.connect.v2.resources.Card.Type") + end end end end diff --git a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_edge_cases_spec.rb b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_edge_cases_spec.rb index a6d434a2e..97e9ac7d5 100644 --- a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_edge_cases_spec.rb +++ b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_edge_cases_spec.rb @@ -163,6 +163,228 @@ def self.enums expect(generated).to include("STATUS_LEGACY = 2;") end + it "requires external enum types to have exactly one enum mapping source" do + results = define_proto_schema do |s| + s.proto_external_types( + "Status" => { + proto: "squareup.connect.v2.Status", + import: "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("must also configure `proto_enum_mappings`")) + end + + it "rejects external enum types with transformed enum mappings" do + results = define_proto_schema do |s| + s.proto_enum_mappings( + "Status" => { + ::Object.new => { + expected_extras: [:LEGACY] + } + } + ) + s.proto_external_types( + "Status" => { + proto: "squareup.connect.v2.Status", + import: "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("must use an empty `proto_enum_mappings` options hash")) + end + + it "rejects external enum types with multiple enum mapping sources" do + results = define_proto_schema do |s| + s.proto_enum_mappings("Status" => {::Object.new => {}, ::Object.new => {}}) + s.proto_external_types( + "Status" => { + proto: "squareup.connect.v2.Status", + import: "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("must use exactly one `proto_enum_mappings` source")) + end + + it "deduplicates imports for repeated external enum references" do + proto_status = ::Class.new do + def self.enums + [::Data.define(:name).new(name: :ACTIVE)] + end + end + + results = define_proto_schema do |s| + s.proto_enum_mappings("Status" => {proto_status => {}}) + s.proto_external_types( + "Status" => { + "proto" => "squareup.connect.v2.Status", + "import" => "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.field "previous_status", "Status" + t.index "accounts" + end + end + + generated = proto_schema_from(results) + expect(generated.scan('import "squareup/connect/v2/status.proto";').size).to eq(1) + expect(generated).to include("squareup.connect.v2.Status status = 2;") + expect(generated).to include("squareup.connect.v2.Status previous_status = 3;") + end + + it "validates external enum values against the ElasticGraph enum values" do + proto_status = ::Class.new do + def self.enums + [ + ::Data.define(:name).new(name: :ACTIVE), + ::Data.define(:name).new(name: :PENDING) + ] + end + end + + results = define_proto_schema do |s| + s.proto_enum_mappings("Status" => {proto_status => {}}) + s.proto_external_types( + "Status" => { + proto: "squareup.connect.v2.Status", + import: "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE", "INACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("values do not match")) + end + + it "rejects external proto type references for non-enum types" do + results = define_proto_schema do |s| + s.proto_external_types( + "Address" => { + proto: "squareup.connect.v2.Address", + import: "squareup/connect/v2/address.proto" + } + ) + + s.object_type "Address" do |t| + t.field "street", "String" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "address", "Address" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("Only enum types are supported")) + end + + it "validates external proto type mapping input type" do + results = define_proto_schema do |s| + s.proto_external_types("bad") + + s.object_type "Account" do |t| + t.field "id", "ID" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("External proto type mappings must be a Hash")) + end + + it "validates per-type external proto type mapping structure" do + results = define_proto_schema do |s| + s.proto_external_types("Status" => "bad") + + s.object_type "Account" do |t| + t.field "id", "ID" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("External proto type mapping for `Status` must be a Hash")) + end + + it "requires external proto type mappings to define proto and import strings" do + results = define_proto_schema do |s| + s.proto_external_types("Status" => {proto: "squareup.connect.v2.Status"}) + + s.object_type "Account" do |t| + t.field "id", "ID" + t.index "accounts" + end + end + + expect { + proto_schema_from(results) + }.to raise_error(Errors::SchemaError, a_string_including("must include a non-empty `import` String")) + end + it "raises on field-number mapping collisions for a message" do results = define_proto_schema do |s| s.configure_proto_field_number_mappings( @@ -332,6 +554,7 @@ def self.enums state: build_fake_state, package_name: "elasticgraph", proto_enums_by_graphql_enum: nil, + proto_external_types: nil, proto_field_number_mappings: nil ) diff --git a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_spec.rb b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_spec.rb index e812e68fb..80745d86a 100644 --- a/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_spec.rb +++ b/elasticgraph-protobuf/spec/unit/elastic_graph/protobuf/schema_definition/schema_spec.rb @@ -493,6 +493,50 @@ def self.enums expect(generated).not_to include("OBSOLETE") end + it "can import and reference an external proto enum type" do + proto_status = ::Class.new do + def self.enums + [ + ::Data.define(:name).new(name: :ACTIVE), + ::Data.define(:name).new(name: :INACTIVE) + ] + end + end + + results = define_proto_schema do |s| + s.proto_enum_mappings("Status" => {proto_status => {}}) + s.proto_external_types( + "Status" => { + proto: "squareup.connect.v2.Status", + import: "squareup/connect/v2/status.proto" + } + ) + + s.enum_type "Status" do |t| + t.values "ACTIVE", "INACTIVE" + end + + s.object_type "Account" do |t| + t.field "id", "ID" + t.field "status", "Status" + t.index "accounts" + end + end + + expect(proto_schema_from(results)).to eq(<<~PROTO) + syntax = "proto3"; + + package elasticgraph; + + import "squareup/connect/v2/status.proto"; + + message Account { + string id = 1; + squareup.connect.v2.Status status = 2; + } + PROTO + end + it "raises when mapped proto enum sources produce inconsistent values" do proto_status_a = ::Class.new do def self.enums