Complete Jsonnet examples and patterns for using sroto.
- Importing Example
- Options Example
- Composed Fields Example
- Custom Options Example
- Importing from .proto Files
- Generating Multiple .proto Files
Imports can either be from .jsonnet files or specified raw (from .proto files):
// filename: import_example.jsonnet
local sroto = import "sroto.libsonnet";
// importing from another sroto file
local example = import "example.jsonnet";
// "importing" from a protobuf file
local Timestamp = {
name: "Timestamp",
filename: "google/protobuf/timestamp.proto",
package: "google.protobuf",
};
sroto.File("import_example.proto", "import_example", {
LogEntry: sroto.Message({
message: sroto.StringField(1),
priority: sroto.Field(example.Priority, 2),
created_at: sroto.Field(Timestamp, 3),
// Well-known types (like Timestamp) are pre-defined in sroto.WKT,
// so the above could be simplified by doing:
updated_at: sroto.Field(sroto.WKT.Timestamp, 4),
}),
})Generated output:
// filename: import_example.proto
// Generated by srotoc. DO NOT EDIT!
syntax = "proto3";
package import_example;
import "example.proto";
import "google/protobuf/timestamp.proto";
message LogEntry {
string message = 1;
example.Priority priority = 2;
google.protobuf.Timestamp created_at = 3;
google.protobuf.Timestamp updated_at = 4;
}This bundles type information together:
- Type name (the enum, message, or custom option)
- Which file the type is declared in
- Package of that file
Suppose we want a UUID field with OpenAPI documentation via grpc-gateway and validation via protoc-gen-validate.
In raw protobuf this is not composable:
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
message User {
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
min_length: 36 // quick quiz: are trailing commas permitted? (answer: no)
},
(validate.rules).string.uuid = true
];
...
}With sroto, create a reusable UUIDField:
// filename: my_custom_fields.libsonnet
local sroto = import "sroto.libsonnet";
{
UUIDField(number):: sroto.StringField(number) {
// note: `options+:` not `options:` - we don't want to overwrite existing options
options+: [
{
type: {
name: "openapiv2_field",
filename: "protoc-gen-openapiv2/options/annotations.proto",
package: "grpc.gateway.protoc_gen_openapiv2.options",
},
value: {
pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
min_length: 36,
},
},
{
// equivalent to (validate.rules).string.uuid = true
type: {
name: "rules",
filename: "validate/validate.proto",
package: "validate",
},
path: "string.uuid",
value: true,
},
],
},
}Then use it anywhere:
// filename: options_example.jsonnet
local sroto = import "sroto.libsonnet";
local my_custom_fields = import "my_custom_fields.libsonnet";
// "import" the custom options
local openapiv2Annotation(name_suffix) = {
name: "openapiv2_%s" % [name_suffix],
filename: "protoc-gen-openapiv2/options/annotations.proto",
package: "grpc.gateway.protoc_gen_openapiv2.options",
};
local api_field_behavior = {
name: "field_behavior",
filename: "google/api/field_behavior.proto",
package: "google.api",
};
local http_api = {
name: "http",
filename: "google/api/annotations.proto",
package: "google.api",
};
sroto.File("options_example.proto", "example", {
User: sroto.Message({
id: my_custom_fields.UUIDField(1),
referrer_user_id: my_custom_fields.UUIDField(2),
}) {
options: [{
type: openapiv2Annotation("schema"),
path: "example",
value: std.manifestJsonEx({
id: "24508faf-20e3-46ca-8b09-d079b595ef0b",
referrer_user_id: "27fa4a4e-5650-484f-87f9-bd915889f92b",
}, " "),
}],
},
GetUserRequest: sroto.Message({
id: sroto.StringField(1) {
options: [{
type: api_field_behavior,
value: [sroto.EnumValueLiteral("REQUIRED")],
}],
},
}),
UserService: sroto.Service({
GetUser: sroto.UnaryMethod("GetUserRequest", "User") {
options: [{
type: http_api,
value: {get: "/users/{id}"},
}],
},
}) {
options: [{
type: openapiv2Annotation("tag"),
path: "description",
value: "UserService is for various operations on users.",
}],
},
}) {
// Built-in options use simple key/value syntax
options: [
{go_package: "github.com/tomlinford/sroto/example/options_example"}
],
}Create reusable custom field types by composing fields with options at multiple levels:
// filename: composed_fields_example.jsonnet
// Example demonstrating field composition with options at multiple levels
local sroto = import "sroto.libsonnet";
// Layer 1: Base custom field with validation options
local StringFieldWithValidation(number, min_len, max_len) =
sroto.StringField(number) {
options+: [{
type: {
name: 'rules',
filename: 'validate/validate.proto',
package: 'validate',
},
path: 'string',
value: {
min_len: min_len,
max_len: max_len,
},
}],
};
// Layer 2: UUID field built on validated string field
local UUIDField(number) =
StringFieldWithValidation(number, 36, 36) {
options+: [{
type: {
name: 'rules',
filename: 'validate/validate.proto',
package: 'validate',
},
path: 'string.uuid',
value: true,
}],
};
// Layer 3: Required UUID field built on UUID field
local RequiredUUIDField(number) =
UUIDField(number) {
options+: [{
type: {
name: 'field_behavior',
filename: 'google/api/field_behavior.proto',
package: 'google.api',
},
value: [sroto.EnumValueLiteral('REQUIRED')],
}],
};
sroto.File('composed_fields_example.proto', 'composed_fields_example', {
User: sroto.Message({
// Uses base custom field
nickname: StringFieldWithValidation(1, 3, 50),
// Uses layer 2 (composed) custom field
user_id: UUIDField(2),
// Uses layer 3 (double composed) custom field
tenant_id: RequiredUUIDField(3),
}),
})Generated output:
// filename: composed_fields_example.proto
// Generated by srotoc. DO NOT EDIT!
syntax = "proto3";
package composed_fields_example;
import "google/api/field_behavior.proto";
import "validate/validate.proto";
message User {
string nickname = 1 [
(validate.rules) = {string: {max_len: 50, min_len: 3}}
];
string user_id = 2 [(validate.rules).string.uuid = true];
string tenant_id = 3 [
(google.api.field_behavior) = REQUIRED,
(validate.rules).string.uuid = true
];
}Note how tenant_id combines options from multiple composition layers - both the REQUIRED field behavior from RequiredUUIDField and the UUID validation from UUIDField. The options+: syntax ensures that options are accumulated rather than overwritten at each composition layer.
When composing fields with options, it's important to understand how options are merged:
- Using
options+:- Appends to the existing options array (recommended for composition) - Using
options:- Replaces the entire options array (rarely what you want)
Options are accumulated in an array, and the protobuf compiler processes them in order. If multiple options set the same field:
- Different types: All options are applied independently
- Same type, different paths: Both options are applied (e.g.,
(validate.rules).string.min_lenand(validate.rules).string.uuid) - Same type, same path: Later options in the array take precedence
Example of option conflict:
// This field has conflicting min_len values
local ConflictingField = sroto.StringField(1) {
options+: [
{ type: rules, path: "string.min_len", value: 5 },
{ type: rules, path: "string.min_len", value: 10 }, // This wins
],
};In practice, avoid conflicts by designing your composed fields carefully. Each composition layer should add complementary options, not conflicting ones.
Define custom options in your schemas:
// filename: custom_options_example.jsonnet
local sroto = import "sroto.libsonnet";
sroto.File("custom_options_example.proto", "custom_options_example", {
SQLTableOptions: sroto.Message({
table_name: sroto.StringField(1),
table_tags: sroto.Field(sroto.WKT.Struct, 2),
table_bin_data: sroto.BytesField(3),
// Obviously using StringValues doesn't really make sense for custom
// options, but the example is here for illustrative purposes.
prev_table_name: sroto.Field(sroto.WKT.StringValue, 4),
next_table_name: sroto.Field(sroto.WKT.StringValue, 5),
}),
sql_table: sroto.CustomMessageOption("SQLTableOptions", 6072),
SQLType: sroto.Enum({
BIGINT: 1,
TEXT: 2,
}),
sql_type: sroto.CustomFieldOption("SQLType", 6073),
})Use the custom options:
// filename: using_custom_options_example.jsonnet
local sroto = import "sroto.libsonnet";
local custom_options_example = import "custom_options_example.jsonnet";
sroto.File("using_custom_options_example.proto", "using_custom_options_example", {
UserTable: sroto.Message({
id: sroto.StringField(1) {options+: [{
// note how we can just use the `sroto` objects directly here:
type: custom_options_example.sql_type,
value: custom_options_example.SQLType.TEXT,
}]},
}) {options+: [{
type: custom_options_example.sql_table,
value: {
table_name: "users",
// Can encode an arbitrary object!
table_tags: sroto.WKT.StructLiteral(
{foo: "bar", baz: ["qux", "quz"], teapot: null},
),
table_bin_data: sroto.BytesLiteral([0, 1, 2, 3, 4, 5, 6, 7, 8]),
prev_table_name: sroto.WKT.StringValueLiteral("old_users"),
next_table_name: null, // This entry will get omitted.
},
}]},
})Generated output:
// filename: using_custom_options_example.proto
// Generated by srotoc. DO NOT EDIT!
syntax = "proto3";
package using_custom_options_example;
import "custom_options_example.proto";
message UserTable {
option (custom_options_example.sql_table) = {
prev_table_name: {value: "old_users"},
table_bin_data: "\x00\x01\x02\x03\x04\x05\x06\a\b",
table_name: "users",
table_tags: {
fields: [
{
key: "baz",
value: {
list_value: {
values: [
{string_value: "qux"},
{string_value: "quz"}
]
}
}
},
{key: "foo", value: {string_value: "bar"}},
{key: "teapot", value: {null_value: NULL_VALUE}}
]
}
};
string id = 1 [(custom_options_example.sql_type) = TEXT];
}Import generated files into hand-written .proto files:
// filename: protobuf_example.proto
syntax = "proto3";
package protobuf_example;
import "example.proto";
message Bug {
string description = 1;
example.Priority priority = 2;
}This lets you adopt sroto gradually while downstream users continue using raw .proto files.
Return an array to generate multiple files from one .jsonnet file:
// filename: multiple_file_example.jsonnet
local sroto = import "sroto.libsonnet";
[
sroto.File("example_%s.proto" % [x], "example_%s" % [x], {
[std.asciiUpper(x)]: sroto.Message({
message: sroto.StringField(1),
})
}) for x in ["a", "b"]
]Generated files:
// filename: example_a.proto
syntax = "proto3";
package example_a;
message A {
string message = 1;
}// filename: example_b.proto
syntax = "proto3";
package example_b;
message B {
string message = 1;
}