Skip to content

Commit e9902f1

Browse files
committed
Add elasticgraph-protobuf schema artifacts
Adds `elasticgraph-protobuf`, a self-contained schema-definition extension that generates Protocol Buffers schema artifacts from an ElasticGraph schema. Running `schema_artifacts:dump` emits: - `schema.proto` — the generated Protobuf schema for the indexed types - `proto_field_numbers.yaml` — a sidecar that reserves field numbers so they stay stable as the schema evolves (including across field renames) Capabilities: - Maps built-in ElasticGraph scalars to proto types, with `proto_field` to configure custom scalars. - Generates messages for object/interface/union types and enums (with a zero-valued `*_UNSPECIFIED` entry), escaping proto reserved words and wrapping nested lists so the output stays valid. - `proto_enum_mappings` reuses enum values already maintained elsewhere, and `proto_external_types` references existing proto enums instead of regenerating them locally. The generator emits `proto3` by default and can emit `proto2` via `syntax: :proto2` (which labels every field `optional`/`repeated`). Arbitrary file-level headers (e.g. `option` declarations) can be injected verbatim via `headers:`, so language-specific options can be set without baking any particular convention into the gem.
1 parent c845040 commit e9902f1

47 files changed

Lines changed: 4026 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CODEBASE_OVERVIEW.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,14 @@ graph LR;
192192
click opensearch-ruby href "https://rubygems.org/gems/opensearch-ruby" "Open on RubyGems.org" _blank;
193193
```
194194

195-
### Extensions (6 gems)
195+
### Extensions (7 gems)
196196

197197
These libraries extend ElasticGraph to provide optional but commonly needed functionality.
198198

199199
* [elasticgraph-apollo](elasticgraph-apollo/README.md): Transforms an ElasticGraph project into an Apollo subgraph.
200200
* [elasticgraph-health_check](elasticgraph-health_check/README.md): Provides a health check for high availability ElasticGraph deployments.
201201
* [elasticgraph-json_ingestion](elasticgraph-json_ingestion/README.md): JSON Schema ingestion support for ElasticGraph.
202+
* [elasticgraph-protobuf](elasticgraph-protobuf/README.md): Generates Protocol Buffers schema artifacts from ElasticGraph schemas.
202203
* [elasticgraph-query_interceptor](elasticgraph-query_interceptor/README.md): Intercepts ElasticGraph datastore queries.
203204
* [elasticgraph-query_registry](elasticgraph-query_registry/README.md): Provides a source-controlled query registry for ElasticGraph applications.
204205
* [elasticgraph-warehouse](elasticgraph-warehouse/README.md): Extends ElasticGraph to support ingestion into a data warehouse.
@@ -218,6 +219,7 @@ graph LR;
218219
elasticgraph-health_check["eg-health_check"];
219220
elasticgraph-datastore_core["eg-datastore_core"];
220221
elasticgraph-json_ingestion["eg-json_ingestion"];
222+
elasticgraph-protobuf["eg-protobuf"];
221223
elasticgraph-query_interceptor["eg-query_interceptor"];
222224
elasticgraph-schema_artifacts["eg-schema_artifacts"];
223225
elasticgraph-query_registry["eg-query_registry"];
@@ -231,6 +233,7 @@ graph LR;
231233
elasticgraph-health_check --> elasticgraph-graphql;
232234
elasticgraph-health_check --> elasticgraph-support;
233235
elasticgraph-json_ingestion --> elasticgraph-support;
236+
elasticgraph-protobuf --> elasticgraph-support;
234237
elasticgraph-query_interceptor --> elasticgraph-graphql;
235238
elasticgraph-query_interceptor --> elasticgraph-schema_artifacts;
236239
elasticgraph-query_registry --> elasticgraph-graphql;
@@ -246,6 +249,7 @@ graph LR;
246249
class elasticgraph-health_check targetGemStyle;
247250
class elasticgraph-datastore_core otherEgGemStyle;
248251
class elasticgraph-json_ingestion targetGemStyle;
252+
class elasticgraph-protobuf targetGemStyle;
249253
class elasticgraph-query_interceptor targetGemStyle;
250254
class elasticgraph-schema_artifacts otherEgGemStyle;
251255
class elasticgraph-query_registry targetGemStyle;

Gemfile.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ PATH
140140
faraday-retry (~> 2.4)
141141
opensearch-ruby (~> 3.4)
142142

143+
PATH
144+
remote: elasticgraph-protobuf
145+
specs:
146+
elasticgraph-protobuf (1.2.1.pre)
147+
elasticgraph-support (= 1.2.1.pre)
148+
143149
PATH
144150
remote: elasticgraph-query_interceptor
145151
specs:
@@ -696,6 +702,7 @@ DEPENDENCIES
696702
elasticgraph-lambda_support (= 1.2.1.pre)!
697703
elasticgraph-local (= 1.2.1.pre)!
698704
elasticgraph-opensearch (= 1.2.1.pre)!
705+
elasticgraph-protobuf (= 1.2.1.pre)!
699706
elasticgraph-query_interceptor (= 1.2.1.pre)!
700707
elasticgraph-query_registry (= 1.2.1.pre)!
701708
elasticgraph-rack (= 1.2.1.pre)!
@@ -788,6 +795,7 @@ CHECKSUMS
788795
elasticgraph-lambda_support (1.2.1.pre)
789796
elasticgraph-local (1.2.1.pre)
790797
elasticgraph-opensearch (1.2.1.pre)
798+
elasticgraph-protobuf (1.2.1.pre)
791799
elasticgraph-query_interceptor (1.2.1.pre)
792800
elasticgraph-query_registry (1.2.1.pre)
793801
elasticgraph-rack (1.2.1.pre)

config/docker_demo/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ COPY elasticgraph-indexer elasticgraph-indexer/
1919
COPY elasticgraph-json_ingestion elasticgraph-json_ingestion/
2020
COPY elasticgraph-local elasticgraph-local/
2121
COPY elasticgraph-opensearch elasticgraph-opensearch/
22+
COPY elasticgraph-protobuf elasticgraph-protobuf/
2223
COPY elasticgraph-query_registry elasticgraph-query_registry/
2324
COPY elasticgraph-rack elasticgraph-rack/
2425
COPY elasticgraph-schema_artifacts elasticgraph-schema_artifacts/

config/site/support/doctest_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
require "elastic_graph/apollo/schema_definition/api_extension"
1010
require "elastic_graph/json_ingestion/schema_definition/api_extension"
11+
require "elastic_graph/protobuf/schema_definition/api_extension"
1112
require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"
1213
require "elastic_graph/schema_definition/api"
1314
require "elastic_graph/schema_definition/schema_artifact_manager"
@@ -53,6 +54,7 @@ module ElasticGraph
5354
"ElasticGraph.define_schema" => [],
5455
"ElasticGraph::Apollo::SchemaDefinition" => [Apollo::SchemaDefinition::APIExtension],
5556
"ElasticGraph::JSONIngestion::SchemaDefinition" => [JSONIngestion::SchemaDefinition::APIExtension],
57+
"ElasticGraph::Protobuf::SchemaDefinition" => [Protobuf::SchemaDefinition::APIExtension],
5658
"ElasticGraph::SchemaDefinition" => [],
5759
"ElasticGraph::Warehouse::SchemaDefinition" => [Warehouse::SchemaDefinition::APIExtension]
5860
}

elasticgraph-protobuf/.rspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../spec_support/subdir_dot_rspec

elasticgraph-protobuf/.yardopts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../config/site/yardopts

elasticgraph-protobuf/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../Gemfile

elasticgraph-protobuf/LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2024 - 2026 Block, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

elasticgraph-protobuf/README.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# ElasticGraph::Protobuf
2+
3+
An ElasticGraph extension that generates Protocol Buffers schema artifacts from ElasticGraph schemas.
4+
It emits `proto3` by default and can emit `proto2`, and supports arbitrary file-level headers (such
5+
as `option` declarations).
6+
7+
## Dependency Diagram
8+
9+
```mermaid
10+
graph LR;
11+
classDef targetGemStyle fill:#FADBD8,stroke:#EC7063,color:#000,stroke-width:2px;
12+
classDef otherEgGemStyle fill:#A9DFBF,stroke:#2ECC71,color:#000;
13+
classDef externalGemStyle fill:#E0EFFF,stroke:#70A1D7,color:#2980B9;
14+
elasticgraph-protobuf["elasticgraph-protobuf"];
15+
class elasticgraph-protobuf targetGemStyle;
16+
elasticgraph-support["elasticgraph-support"];
17+
elasticgraph-protobuf --> elasticgraph-support;
18+
class elasticgraph-support otherEgGemStyle;
19+
```
20+
21+
## Usage
22+
23+
First, add `elasticgraph-protobuf` to your `Gemfile`, alongside the other ElasticGraph gems:
24+
25+
```diff
26+
diff --git a/Gemfile b/Gemfile
27+
index 4a5ef1e..5c16c2b 100644
28+
--- a/Gemfile
29+
+++ b/Gemfile
30+
@@ -8,6 +8,7 @@ gem "elasticgraph-query_registry", *elasticgraph_details
31+
32+
# Can be elasticgraph-elasticsearch or elasticgraph-opensearch based on the datastore you want to use.
33+
gem "elasticgraph-opensearch", *elasticgraph_details
34+
+gem "elasticgraph-protobuf", *elasticgraph_details
35+
36+
gem "httpx", "~> 1.3"
37+
38+
```
39+
40+
Next, update your `Rakefile` so that `ElasticGraph::Protobuf::SchemaDefinition::APIExtension` is
41+
included in the schema-definition extension modules:
42+
43+
```diff
44+
diff --git a/Rakefile b/Rakefile
45+
index 2943335..26633c3 100644
46+
--- a/Rakefile
47+
+++ b/Rakefile
48+
@@ -3,5 +3,6 @@
49+
require "elastic_graph/json_ingestion/schema_definition/api_extension"
50+
require "elastic_graph/local/rake_tasks"
51+
+require "elastic_graph/protobuf/schema_definition/api_extension"
52+
require "elastic_graph/query_registry/rake_tasks"
53+
require "rspec/core/rake_task"
54+
require "standard/rake"
55+
@@ -16,6 +17,7 @@ ElasticGraph::Local::RakeTasks.new(
56+
# Determines casing of field names. Can be either `:camelCase` or `:snake_case`.
57+
tasks.schema_element_name_form = :camelCase
58+
tasks.schema_definition_extension_modules << ElasticGraph::JSONIngestion::SchemaDefinition::APIExtension
59+
+ tasks.schema_definition_extension_modules << ElasticGraph::Protobuf::SchemaDefinition::APIExtension
60+
61+
# Customizes the names of fields generated by ElasticGraph.
62+
tasks.schema_element_name_overrides = {
63+
```
64+
65+
Then opt into proto generation from your schema definition:
66+
67+
```ruby
68+
# in config/schema/protobuf.rb
69+
70+
ElasticGraph.define_schema do |schema|
71+
schema.proto_schema_artifacts package_name: "myapp.events.v1"
72+
end
73+
```
74+
75+
After running `bundle exec rake schema_artifacts:dump`, ElasticGraph will generate:
76+
77+
- `schema.proto`
78+
- `proto_field_numbers.yaml`
79+
80+
## Schema Definition Options
81+
82+
### Protobuf Syntax (`proto2` / `proto3`)
83+
84+
`proto_schema_artifacts` emits `proto3` by default. Pass `syntax: :proto2` to emit a proto2 file
85+
instead (every field is then labeled `optional` or `repeated`). This is useful when the generated
86+
messages need to reference proto2 types — for example, `protoc` forbids a `proto3` message from
87+
referencing a `proto2` enum:
88+
89+
```ruby
90+
ElasticGraph.define_schema do |schema|
91+
schema.proto_schema_artifacts package_name: "myapp.events.v1", syntax: :proto2
92+
end
93+
```
94+
95+
### Custom Headers
96+
97+
Pass `headers:` an array of strings to inject file-level lines (such as `option` declarations)
98+
verbatim, as a contiguous section immediately after the `package` declaration. This lets you set
99+
language-specific options without the gem baking in any particular convention:
100+
101+
```ruby
102+
ElasticGraph.define_schema do |schema|
103+
schema.proto_schema_artifacts(
104+
package_name: "myapp.events.v1",
105+
headers: [
106+
%(option java_package = "com.myapp.events";),
107+
"option java_multiple_files = true;"
108+
]
109+
)
110+
end
111+
```
112+
113+
produces:
114+
115+
```proto
116+
syntax = "proto3";
117+
118+
package myapp.events.v1;
119+
120+
option java_package = "com.myapp.events";
121+
option java_multiple_files = true;
122+
123+
// ...messages...
124+
```
125+
126+
### Custom Scalar Types
127+
128+
Built-in ElasticGraph scalar types are automatically mapped to proto scalar types.
129+
For custom scalar types, use `proto_field` to define the proto scalar type:
130+
131+
```ruby
132+
# in config/schema/money.rb
133+
134+
ElasticGraph.define_schema do |schema|
135+
schema.scalar_type "Money" do |t|
136+
t.mapping type: "long"
137+
t.json_schema type: "integer"
138+
t.proto_field type: "int64"
139+
end
140+
end
141+
```
142+
143+
### Sourcing Enum Values From Existing Protobuf Mappings
144+
145+
If your project already maintains GraphQL-to-proto enum mappings (for example in tests),
146+
you can reuse them for proto schema generation:
147+
148+
```ruby
149+
# in config/schema/proto_enum_mappings.rb
150+
151+
ElasticGraph.define_schema do |schema|
152+
schema.proto_enum_mappings(
153+
SalesEg::ProtoEnumMappings::PROTO_ENUMS_BY_GRAPHQL_ENUM
154+
) if defined?(SalesEg::ProtoEnumMappings)
155+
end
156+
```
157+
158+
When a mapping exists for an enum, `elasticgraph-protobuf` uses the mapped proto enum(s)
159+
as the source of enum values (respecting `exclusions`, `expected_extras`, and `name_transform`).
160+
161+
### Referencing Existing Protobuf Types
162+
163+
For enums that exactly match a canonical proto enum, you can import and reference
164+
the existing proto type instead of generating a duplicate local enum:
165+
166+
```ruby
167+
ElasticGraph.define_schema do |schema|
168+
schema.proto_enum_mappings(
169+
"CardType" => {
170+
Squareup::Connect::V2::Resources::Card::Type => {}
171+
}
172+
)
173+
174+
schema.proto_external_types(
175+
"CardType" => {
176+
proto: "squareup.connect.v2.resources.Card.Type",
177+
import: "squareup/connect/v2/resources/card.proto"
178+
}
179+
)
180+
end
181+
```
182+
183+
External type references currently support enums only. The matching
184+
`proto_enum_mappings` entry must have exactly one source and no transform options;
185+
otherwise the enum stays generated locally so value curation remains explicit.
186+
187+
### Stable Field Numbers
188+
189+
`schema_artifacts:dump` automatically reads and writes `proto_field_numbers.yaml`
190+
in the schema artifacts directory. Existing numbers stay fixed even if field order
191+
changes, and new fields get the next available numbers.
192+
193+
`schema.proto` always uses the public GraphQL field names. When a field uses a
194+
different `name_in_index`, the sidecar YAML stores that override privately:
195+
196+
```yaml
197+
messages:
198+
Widget:
199+
fields:
200+
id: 1
201+
display_name:
202+
field_number: 2
203+
name_in_index: displayName
204+
```
205+
206+
If a field is renamed with `field.renamed_from`, `elasticgraph-protobuf` reuses the
207+
existing field number under the new public field name.
208+
209+
## Type Mappings
210+
211+
The generated `schema.proto` uses these built-in scalar mappings:
212+
213+
| ElasticGraph Type | Protobuf Type |
214+
|-------------------|------------|
215+
| `Boolean` | `bool` |
216+
| `Cursor` | `string` |
217+
| `Date` | `string` |
218+
| `DateTime` | `string` |
219+
| `Float` | `double` |
220+
| `ID` | `string` |
221+
| `Int` | `int32` |
222+
| `JsonSafeLong` | `int64` |
223+
| `LocalTime` | `string` |
224+
| `LongString` | `int64` |
225+
| `String` | `string` |
226+
| `TimeZone` | `string` |
227+
| `Untyped` | `string` |
228+
229+
Additionally:
230+
- List types become `repeated` fields.
231+
- Nested list types generate wrapper messages so the output remains valid `proto3`.
232+
- Enum types generate `enum` definitions whose values are prefixed with the enum type name in `UPPER_SNAKE_CASE`, including a zero-valued `*_UNSPECIFIED` entry.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2024 - 2026 Block, Inc.
2+
#
3+
# Use of this source code is governed by an MIT-style
4+
# license that can be found in the LICENSE file or at
5+
# https://opensource.org/licenses/MIT.
6+
#
7+
# frozen_string_literal: true
8+
9+
require_relative "../elasticgraph-support/lib/elastic_graph/version"
10+
11+
Gem::Specification.new do |spec|
12+
spec.name = "elasticgraph-protobuf"
13+
spec.version = ElasticGraph::VERSION
14+
spec.authors = ["Josh Wilson", "Myron Marston", "Block Engineering"]
15+
spec.email = ["joshuaw@squareup.com"]
16+
spec.homepage = "https://block.github.io/elasticgraph/"
17+
spec.license = "MIT"
18+
spec.summary = "Generates Protocol Buffers schema artifacts from ElasticGraph schemas."
19+
20+
spec.metadata = {
21+
"bug_tracker_uri" => "https://github.com/block/elasticgraph/issues",
22+
"changelog_uri" => "https://github.com/block/elasticgraph/releases/tag/v#{ElasticGraph::VERSION}",
23+
"documentation_uri" => "https://block.github.io/elasticgraph/api-docs/v#{ElasticGraph::VERSION}/",
24+
"homepage_uri" => "https://block.github.io/elasticgraph/",
25+
"source_code_uri" => "https://github.com/block/elasticgraph/tree/v#{ElasticGraph::VERSION}/#{spec.name}",
26+
"gem_category" => "extension"
27+
}
28+
29+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
30+
`git ls-files -z`.split("\x0").reject do |f|
31+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features|sig)/|\.(?:git|travis|circleci)|appveyor)})
32+
end - [".rspec", "Gemfile", ".yardopts"]
33+
end
34+
35+
spec.required_ruby_version = [">= 3.4", "< 4.1"]
36+
37+
spec.add_dependency "elasticgraph-support", ElasticGraph::VERSION
38+
39+
spec.add_development_dependency "elasticgraph-json_ingestion", ElasticGraph::VERSION
40+
spec.add_development_dependency "elasticgraph-schema_definition", ElasticGraph::VERSION
41+
end

0 commit comments

Comments
 (0)