Move Camino package codegen into package runtime

This commit is contained in:
Timothy J. Aveni
2026-07-13 21:49:18 -07:00
parent b7cf9eaa07
commit 740bbc7aff
9 changed files with 1505 additions and 7 deletions
+809
View File
@@ -0,0 +1,809 @@
import fs from "node:fs";
import path from "node:path";
import protobuf from "protobufjs";
import type {
Cardinality,
ClassSchema,
ConflictStrategy,
FieldSchema,
FieldStorage,
FieldStorageKind,
FunctionRef,
InterfaceFieldContract,
InterfaceImplementation,
InterfaceSchema,
MethodSchema,
OperationSchema,
OperationServiceSchema,
MigrationSpec,
SchemaCompileResult,
SymbolRef,
TypeRef,
} from "./schema-ir.js";
type OptionBag = Record<string, unknown>;
const CLASS_OPTION = "(camino.class)";
const INTERFACE_OPTION = "(camino.interface)";
const IMPLEMENTS_OPTION = "(camino.implements)";
const CONSTRUCTOR_OPTION = "(camino.constructor)";
const METHOD_OPTION = "(camino.method)";
const MIGRATION_OPTION = "(camino.migration)";
const CONFLICT_OPTION = "(camino.conflict)";
const EDGE_OPTION = "(camino.edge)";
const FIELD_STORAGE_OPTION = "(camino.field_storage)";
const FIELD_OPS_OPTION = "(camino.field_ops)";
const INTERFACE_FIELD_OPTION = "(camino.interface_field)";
const DISPLAY_LABEL_OPTION = "(camino.display_label)";
const IMPL_OPTION = "(camino.impl)";
const SCHEMA_NAMESPACE_OPTION = "(camino.schema_namespace)";
const SCHEMA_VERSION_OPTION = "(camino.schema_version)";
const enumName = (value: unknown) =>
String(value)
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/-/g, "_")
.toUpperCase();
const lowerEnumName = (value: unknown) => enumName(value).toLowerCase();
const getOption = (
options: OptionBag | undefined,
name: string,
): unknown => {
if (!options) {
return undefined;
}
if (Object.hasOwn(options, name)) {
return options[name];
}
const bareName = name.replace(/^\((.*)\)$/, "$1");
if (Object.hasOwn(options, bareName)) {
return options[bareName];
}
return undefined;
};
const getParsedOptions = (
reflection: protobuf.ReflectionObject,
): OptionBag[] => {
const parsedOptions = (reflection as unknown as { parsedOptions?: unknown })
.parsedOptions;
return Array.isArray(parsedOptions)
? parsedOptions.filter(
(option): option is OptionBag =>
Boolean(option) && typeof option === "object" && !Array.isArray(option),
)
: [];
};
const getReflectionOptions = (
reflection: protobuf.ReflectionObject,
name: string,
): unknown[] => {
const parsedValues = getParsedOptions(reflection).flatMap((option) => {
const value = getOption(option, name);
return value === undefined ? [] : [value];
});
if (parsedValues.length > 0) {
return parsedValues;
}
const value = getOption(reflection.options, name);
return value === undefined ? [] : [value];
};
const getReflectionOption = (
reflection: protobuf.ReflectionObject,
name: string,
): unknown => getReflectionOptions(reflection, name)[0];
const asObject = (value: unknown): OptionBag | undefined =>
value && typeof value === "object" && !Array.isArray(value)
? (value as OptionBag)
: undefined;
const asArray = (value: unknown): unknown[] =>
Array.isArray(value) ? value : value === undefined ? [] : [value];
const readString = (
object: OptionBag | undefined,
key: string,
): string | undefined => {
const value = object?.[key];
return typeof value === "string" ? value : undefined;
};
const readNumber = (
object: OptionBag | undefined,
key: string,
): number | undefined => {
const value = object?.[key];
return typeof value === "number" && Number.isFinite(value)
? value
: undefined;
};
const readBoolean = (
object: OptionBag | undefined,
key: string,
): boolean | undefined => {
const value = object?.[key];
return typeof value === "boolean" ? value : undefined;
};
const symbolRefFromOption = (
value: unknown,
fallback: SymbolRef,
): SymbolRef => {
const object = asObject(value);
if (!object) {
return fallback;
}
return {
namespace: readString(object, "namespace") ?? fallback.namespace,
name: readString(object, "name") ?? fallback.name,
version: readString(object, "version") ?? fallback.version,
...(readString(object, "hash")
? { hash: readString(object, "hash") }
: {}),
};
};
const typeRefFromOption = (
value: unknown,
fallbackVersion: string,
): TypeRef | undefined => {
const object = asObject(value);
if (!object) {
return undefined;
}
const symbol = asObject(object.symbol)
? symbolRefFromOption(object.symbol, {
namespace: "",
name: "",
version: fallbackVersion,
})
: undefined;
const protoType = readString(object, "proto_type")?.replace(/^\./, "");
const symbolProtoType =
symbol?.namespace && symbol.name ? `${symbol.namespace}.${symbol.name}` : "";
const resolvedProtoType = protoType ?? symbolProtoType;
if (!resolvedProtoType) {
return undefined;
}
return {
protoType: resolvedProtoType,
...(symbol?.namespace && symbol.name ? { symbol } : {}),
};
};
const interfaceFieldContractFromOption = (
value: unknown,
schemaVersion: string,
): InterfaceFieldContract | undefined => {
const object = asObject(value);
if (!object) {
return undefined;
}
const type = typeRefFromOption(object.type, schemaVersion);
return {
required: readBoolean(object, "required") ?? false,
readable: readBoolean(object, "readable") ?? false,
writable: readBoolean(object, "writable") ?? false,
watchable: readBoolean(object, "watchable") ?? false,
...(readString(object, "type_param")
? { typeParam: readString(object, "type_param") }
: {}),
...(type ? { type } : {}),
};
};
const functionRefFromOption = (value: unknown): FunctionRef => {
const object = asObject(value);
if (!object) {
throw new Error("Expected function ref option object");
}
const packageNamespace = readString(object, "package_namespace");
const packageName = readString(object, "package_name");
const symbol = readString(object, "symbol");
if (!packageNamespace || !packageName || !symbol) {
throw new Error("Function ref requires package_namespace, package_name, and symbol");
}
return {
packageNamespace,
packageName,
symbol,
...(readString(object, "operation")
? { operation: readString(object, "operation") }
: {}),
...(readString(object, "version_ref")
? { versionRef: readString(object, "version_ref") }
: {}),
};
};
const conflictFromOption = (value: unknown): ConflictStrategy => {
const normalized = enumName(value);
if (!value || normalized === "CONFLICT_STRATEGY_UNSPECIFIED") {
return "preserve_conflicts";
}
if (normalized === "REPLACE") {
return "replace";
}
if (normalized === "PRESERVE_CONFLICTS") {
return "preserve_conflicts";
}
if (normalized === "CRDT") {
return "crdt";
}
throw new Error(`Unknown Camino conflict strategy: ${String(value)}`);
};
const cardinalityFromOption = (value: unknown): Cardinality => {
const normalized = lowerEnumName(value);
if (!value || normalized === "cardinality_unspecified") {
return "many";
}
if (
normalized === "optional_one" ||
normalized === "exactly_one" ||
normalized === "many" ||
normalized === "many_unique" ||
normalized === "many_ordered"
) {
return normalized;
}
throw new Error(`Unknown Camino cardinality: ${String(value)}`);
};
const fieldStorageKindFromOption = (value: unknown): FieldStorageKind => {
if (value === 5) {
return "static_final";
}
const normalized = lowerEnumName(value);
if (!value || normalized === "field_storage_kind_unspecified") {
return "stored";
}
if (
normalized === "stored" ||
normalized === "derived" ||
normalized === "lazy" ||
normalized === "external" ||
normalized === "static_final"
) {
return normalized;
}
throw new Error(`Unknown Camino field storage kind: ${String(value)}`);
};
const fieldStorageFromOption = (value: unknown): FieldStorage => {
const object = asObject(value);
if (!object) {
return {
kind: "stored",
cache: false,
};
}
const kind = fieldStorageKindFromOption(object.kind);
const resolver = object.resolver === undefined
? undefined
: functionRefFromOption(object.resolver);
if ((kind === "derived" || kind === "lazy" || kind === "external") && !resolver) {
throw new Error(`${kind} field storage requires resolver`);
}
return {
kind,
...(resolver ? { resolver } : {}),
cache: object.cache === true,
};
};
const fieldOpsFromOption = (value: unknown) => {
const object = asObject(value);
if (!object) {
return undefined;
}
const implementation = enumName(object.implementation);
if (implementation !== "SERVICE") {
throw new Error(
`Unsupported Camino field implementation: ${String(object.implementation)}`,
);
}
const service = readString(object, "service");
if (!service) {
throw new Error("SERVICE field_ops requires service");
}
return {
implementation: "service" as const,
service,
};
};
const extractFileDefaults = (sourceFile: string) => {
const text = fs.readFileSync(sourceFile, "utf8");
const packageMatch = /^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(text);
const namespaceMatch =
/^\s*option\s+\(camino\.schema_namespace\)\s*=\s*"([^"]+)"\s*;/m.exec(
text,
);
const versionMatch =
/^\s*option\s+\(camino\.schema_version\)\s*=\s*"([^"]+)"\s*;/m.exec(text);
return {
packageName: packageMatch?.[1] ?? "",
schemaNamespace: namespaceMatch?.[1] ?? packageMatch?.[1] ?? "",
schemaVersion: versionMatch?.[1] ?? "1",
};
};
const protoSymbolFromReflection = (
reflection: protobuf.ReflectionObject | null,
fallbackVersion: string,
): SymbolRef | undefined => {
if (!reflection?.fullName) {
return undefined;
}
const parts = reflection.fullName.replace(/^\./, "").split(".");
const name = parts.pop();
if (!name) {
return undefined;
}
return {
namespace: parts.join("."),
name,
version: fallbackVersion,
};
};
const typeRefForField = (
field: protobuf.Field,
schemaVersion: string,
): TypeRef => {
const symbol = protoSymbolFromReflection(field.resolvedType, schemaVersion);
const enumValues =
field.resolvedType instanceof protobuf.Enum
? Object.entries(field.resolvedType.values)
.filter(([_name, value]) => value !== 0)
.map(([name]) => name)
: undefined;
return {
protoType: field.type,
...(symbol ? { symbol } : {}),
...(enumValues ? { enumValues } : {}),
};
};
const typeRefForType = (
type: protobuf.Type | undefined,
fallbackType: string,
schemaVersion: string,
): TypeRef => {
const symbol = protoSymbolFromReflection(type ?? null, schemaVersion);
return {
protoType: type?.fullName?.replace(/^\./, "") ?? fallbackType,
...(symbol ? { symbol } : {}),
};
};
const fieldSchemaFromField = (
field: protobuf.Field,
schemaVersion: string,
): FieldSchema => {
const storage = fieldStorageFromOption(
getReflectionOption(field, FIELD_STORAGE_OPTION),
);
const ops = fieldOpsFromOption(getReflectionOption(field, FIELD_OPS_OPTION));
if (ops && storage.kind !== "stored" && storage.kind !== "static_final") {
throw new Error(
`Field ${field.fullName} cannot declare both field_ops and package-resolved storage`,
);
}
if (storage.kind === "static_final" && !ops) {
throw new Error(`Field ${field.fullName} with static_final storage requires field_ops`);
}
const interfaceContract = interfaceFieldContractFromOption(
getReflectionOption(field, INTERFACE_FIELD_OPTION),
schemaVersion,
);
return {
name: field.name,
tag: field.id,
type: typeRefForField(field, schemaVersion),
conflict: conflictFromOption(getReflectionOption(field, CONFLICT_OPTION)),
repeated: field.repeated,
optional: field.optional,
storage,
...(ops ? { ops } : {}),
...(interfaceContract ? { interfaceContract } : {}),
isDisplayLabel: getReflectionOption(field, DISPLAY_LABEL_OPTION) === true,
};
};
const isStringType = (type: TypeRef) =>
type.protoType === "string" || type.protoType === "google.protobuf.StringValue";
const validateDisplayLabelFields = (
type: protobuf.Type,
fields: FieldSchema[],
operationServices: OperationServiceSchema[],
) => {
const displayLabelFields = fields.filter((field) => field.isDisplayLabel);
if (displayLabelFields.length > 1) {
throw new Error(
`Class ${type.fullName} declares multiple Camino display label fields`,
);
}
const displayLabelField = displayLabelFields[0];
if (!displayLabelField) {
return;
}
if (!displayLabelField.ops) {
if (!isStringType(displayLabelField.type)) {
throw new Error(
`Display label field ${type.fullName}.${displayLabelField.name} must have string type`,
);
}
return;
}
const serviceName = displayLabelField.ops.service.replace(/^\./, "");
const getOperation = operationServices
.find((service) => service.fullName === serviceName)
?.operations.find((operation) => operation.name === "Get");
if (!getOperation || !isStringType(getOperation.outputType)) {
throw new Error(
`Display label field ${type.fullName}.${displayLabelField.name} must resolve to string`,
);
}
};
const typeBindingFromOption = (
value: unknown,
schemaVersion: string,
) => {
const object = asObject(value);
if (!object) {
return undefined;
}
const name = readString(object, "name");
const type = typeRefFromOption(object.type, schemaVersion);
if (!name || !type) {
throw new Error("Interface type_binding requires name and type");
}
return { name, type };
};
const interfaceImplementationFromOption = (
value: unknown,
defaults: ReturnType<typeof extractFileDefaults>,
): InterfaceImplementation => {
const object = asObject(value);
if (!object) {
throw new Error("Expected implements option object");
}
const interfaceId = symbolRefFromOption(object.interface, {
namespace: defaults.schemaNamespace,
name: "",
version: defaults.schemaVersion,
});
if (!interfaceId.name) {
throw new Error("implements option requires interface name");
}
return {
interface: interfaceId,
typeBindings: asArray(object.type_binding)
.map((binding) => typeBindingFromOption(binding, defaults.schemaVersion))
.filter((binding): binding is NonNullable<typeof binding> =>
Boolean(binding),
),
};
};
const serviceName = (service: protobuf.Service) =>
service.fullName.replace(/^\./, "");
const operationFromMethod = (
method: protobuf.Method,
schemaVersion: string,
): OperationSchema => {
const fn = functionRefFromOption(getReflectionOption(method, IMPL_OPTION));
const methodWithTypes = method as protobuf.Method & {
resolvedRequestType?: protobuf.Type;
resolvedResponseType?: protobuf.Type;
};
return {
name: method.name,
inputType: typeRefForType(
methodWithTypes.resolvedRequestType,
method.requestType,
schemaVersion,
),
outputType: typeRefForType(
methodWithTypes.resolvedResponseType,
method.responseType,
schemaVersion,
),
function: fn,
};
};
const operationServiceFromService = (
service: protobuf.Service,
schemaVersion: string,
): OperationServiceSchema => ({
name: service.name,
fullName: serviceName(service),
operations: service.methodsArray.map((method) =>
operationFromMethod(method, schemaVersion),
),
});
const classSchemaFromType = (
type: protobuf.Type,
sourceFile: string,
defaults: ReturnType<typeof extractFileDefaults>,
operationServices: OperationServiceSchema[],
): ClassSchema | undefined => {
const classOption = asObject(getReflectionOption(type, CLASS_OPTION));
if (!classOption) {
return undefined;
}
const fallbackClassRef = {
namespace: defaults.schemaNamespace,
name: type.name,
version: defaults.schemaVersion,
};
const classId = symbolRefFromOption(classOption.id, fallbackClassRef);
const classVersion = readNumber(classOption, "version") ?? Number(classId.version);
if (!Number.isInteger(classVersion) || classVersion <= 0) {
throw new Error(`Class ${type.fullName} has invalid Camino class version`);
}
const fields = type.fieldsArray.map((field) =>
fieldSchemaFromField(field, defaults.schemaVersion),
);
validateDisplayLabelFields(type, fields, operationServices);
const edges = type.fieldsArray.flatMap((field) => {
const edgeOption = asObject(getReflectionOption(field, EDGE_OPTION));
if (!edgeOption) {
return [];
}
const targetClass = symbolRefFromOption(edgeOption.target, {
namespace: defaults.schemaNamespace,
name: "",
version: defaults.schemaVersion,
});
if (!targetClass.name) {
throw new Error(`Edge field ${type.fullName}.${field.name} requires target`);
}
const cardinality = cardinalityFromOption(edgeOption.cardinality);
const sourceType = typeRefForField(field, defaults.schemaVersion);
if (
sourceType.symbol?.namespace !== "camino" ||
sourceType.symbol.name !== "Ref"
) {
throw new Error(`Edge field ${type.fullName}.${field.name} must use camino.Ref`);
}
const isMany =
cardinality === "many" ||
cardinality === "many_unique" ||
cardinality === "many_ordered";
if (isMany && !field.repeated) {
throw new Error(`Edge field ${type.fullName}.${field.name} must be repeated for ${cardinality}`);
}
if (!isMany && field.repeated) {
throw new Error(`Edge field ${type.fullName}.${field.name} must not be repeated for ${cardinality}`);
}
const edgeId = symbolRefFromOption(edgeOption.id, {
namespace: defaults.schemaNamespace,
name: `${type.name}.${field.name}`,
version: defaults.schemaVersion,
});
return [
{
id: edgeId,
sourceField: field.name,
fromClass: classId,
toClass: targetClass,
cardinality,
...(readString(edgeOption, "inverse")
? { inverse: readString(edgeOption, "inverse") }
: {}),
fields: [],
},
];
});
const methods: MethodSchema[] = getReflectionOptions(type, METHOD_OPTION)
.map(asObject)
.filter((option): option is OptionBag => Boolean(option))
.map((option) => {
const name = readString(option, "name");
if (!name) {
throw new Error(`Method option on ${type.fullName} requires name`);
}
return {
name,
function: functionRefFromOption(option.function),
};
});
const migrations: MigrationSpec[] = getReflectionOptions(type, MIGRATION_OPTION)
.map(asObject)
.filter((option): option is OptionBag => Boolean(option))
.map((option) => {
const fromVersion = readNumber(option, "from_version");
const toVersion = readNumber(option, "to_version");
if (!fromVersion || !toVersion) {
throw new Error(`Migration option on ${type.fullName} requires versions`);
}
return {
fromVersion,
toVersion,
function: functionRefFromOption(option.function),
};
});
const constructorOption = asObject(
getReflectionOption(type, CONSTRUCTOR_OPTION),
);
const constructorSpec = constructorOption
? { function: functionRefFromOption(constructorOption.function) }
: undefined;
return {
id: classId,
version: classVersion,
fields,
edges,
methods,
migrations,
operationServices,
implements: getReflectionOptions(type, IMPLEMENTS_OPTION).map((option) =>
interfaceImplementationFromOption(option, defaults),
),
...(constructorSpec ? { constructorSpec } : {}),
sourceFile,
protoMessage: type.fullName,
};
};
const interfaceSchemaFromType = (
type: protobuf.Type,
sourceFile: string,
defaults: ReturnType<typeof extractFileDefaults>,
): InterfaceSchema | undefined => {
const interfaceOption = asObject(getReflectionOption(type, INTERFACE_OPTION));
if (!interfaceOption) {
return undefined;
}
const classOption = asObject(getReflectionOption(type, CLASS_OPTION));
if (classOption) {
throw new Error(`${type.fullName} cannot be both a Camino class and interface`);
}
const fallbackInterfaceRef = {
namespace: defaults.schemaNamespace,
name: type.name,
version: defaults.schemaVersion,
};
const interfaceId = symbolRefFromOption(
interfaceOption.id,
fallbackInterfaceRef,
);
const interfaceVersion =
readNumber(interfaceOption, "version") ?? Number(interfaceId.version);
if (!Number.isInteger(interfaceVersion) || interfaceVersion <= 0) {
throw new Error(`Interface ${type.fullName} has invalid Camino interface version`);
}
return {
id: interfaceId,
version: interfaceVersion,
fields: type.fieldsArray.map((field) =>
fieldSchemaFromField(field, defaults.schemaVersion),
),
sourceFile,
protoMessage: type.fullName,
};
};
const walkTypes = (
namespace: protobuf.NamespaceBase,
visit: (type: protobuf.Type) => void,
) => {
for (const nested of namespace.nestedArray) {
if (nested instanceof protobuf.Type) {
visit(nested);
walkTypes(nested, visit);
} else if (nested instanceof protobuf.Namespace) {
walkTypes(nested, visit);
}
}
};
const walkServices = (
namespace: protobuf.NamespaceBase,
visit: (service: protobuf.Service) => void,
) => {
for (const nested of namespace.nestedArray) {
if (nested instanceof protobuf.Service) {
visit(nested);
} else if (nested instanceof protobuf.Namespace) {
walkServices(nested, visit);
}
}
};
export const compileCaminoSchema = async (
sourceFile: string,
): Promise<SchemaCompileResult> => {
const absoluteSourceFile = path.resolve(sourceFile);
const defaults = extractFileDefaults(absoluteSourceFile);
const root = new protobuf.Root();
const envIncludeDirs = [
process.env.QUIXOS_PROTO_PATH,
process.env.CAMINO_PROTO_PATH,
]
.filter((value): value is string => Boolean(value))
.join(path.delimiter)
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => path.resolve(entry));
const includeDirs = [
path.dirname(absoluteSourceFile),
path.resolve("../../quixos-protocol/proto"),
path.resolve("quixos-protocol/proto"),
path.resolve("proto"),
...envIncludeDirs,
];
root.resolvePath = (origin, target) => {
if (path.isAbsolute(target) && fs.existsSync(target)) {
return target;
}
const originDir = origin ? path.dirname(origin) : undefined;
const candidates = [
...(originDir ? [path.resolve(originDir, target)] : []),
...includeDirs.map((dir) => path.resolve(dir, target)),
];
const found = candidates.find((candidate) => fs.existsSync(candidate));
return found ?? target;
};
await root.load(absoluteSourceFile, { keepCase: true });
root.resolveAll();
const operationServices: OperationServiceSchema[] = [];
walkServices(root, (service) => {
operationServices.push(
operationServiceFromService(service, defaults.schemaVersion),
);
});
const classes: ClassSchema[] = [];
const interfaces: InterfaceSchema[] = [];
walkTypes(root, (type) => {
const interfaceSchema = interfaceSchemaFromType(
type,
absoluteSourceFile,
defaults,
);
if (interfaceSchema) {
interfaces.push(interfaceSchema);
}
const classSchema = classSchemaFromType(
type,
absoluteSourceFile,
defaults,
operationServices,
);
if (classSchema) {
classes.push(classSchema);
}
});
return {
sourceFile: absoluteSourceFile,
classes,
interfaces,
};
};