// Command settingsgen emits typed bindings for the settings contract. // // One generator for every language rather than one per repo: the whole point of // the contract is that four codebases agree on keys, types, scopes and // defaults, and four independently-written generators would be four chances to // disagree. Each client repo vendors the manifest and runs this to regenerate. // // Usage: // // settingsgen -lang go -out internal/settingskeys/keys.go // settingsgen -lang ts -out web/src/lib/settingsContract.ts // settingsgen -lang kotlin -out -package org.siloserver.silo.model.settings // settingsgen -lang swift -out package main import ( "bytes" "encoding/json" "flag" "fmt" "os" "sort" "strings" "github.com/Silo-Server/silo-server/internal/settingscontract" ) func main() { lang := flag.String("lang", "", "go, ts, kotlin or swift") out := flag.String("out", "", "file to write (default stdout)") pkg := flag.String("package", "", "package or namespace for the generated code") flag.Parse() contract, err := settingscontract.Load() if err != nil { fail("loading contract: %v", err) } var body []byte switch *lang { case "go": body, err = generateGo(contract, defaultString(*pkg, "settingskeys")) case "ts": body, err = generateTypeScript(contract) case "kotlin": body, err = generateKotlin(contract, defaultString(*pkg, "org.siloserver.silo.model.settings")) case "swift": body, err = generateSwift(contract) default: fail("unknown -lang %q: want go, ts, kotlin or swift", *lang) } if err != nil { fail("generating %s: %v", *lang, err) } if *out == "" { _, _ = os.Stdout.Write(body) return } if err := os.WriteFile(*out, body, 0o644); err != nil { //nolint:gosec // generated source fail("writing %s: %v", *out, err) } } func fail(format string, args ...any) { fmt.Fprintf(os.Stderr, "settingsgen: "+format+"\n", args...) os.Exit(1) } func defaultString(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } // remoteAndLocal returns every definition, sorted by key so the output is // stable: a generator whose output depends on manifest authoring order would // produce spurious diffs on every unrelated manifest edit. func sortedDefinitions(contract *settingscontract.Manifest) []*settingscontract.Definition { defs := make([]*settingscontract.Definition, 0, len(contract.Definitions)) for i := range contract.Definitions { defs = append(defs, &contract.Definitions[i]) } sort.Slice(defs, func(i, j int) bool { return defs[i].Key < defs[j].Key }) return defs } // identifierFor turns a dotted key into a language identifier: // playback.subtitle_language becomes PlaybackSubtitleLanguage. func identifierFor(key string) string { var out strings.Builder for _, part := range strings.FieldsFunc(key, func(r rune) bool { return r == '.' || r == '_' || r == '-' }) { out.WriteString(strings.ToUpper(part[:1])) out.WriteString(part[1:]) } return out.String() } // screamingCase turns a dotted key into PLAYBACK_SUBTITLE_LANGUAGE. func screamingCase(key string) string { replaced := strings.NewReplacer(".", "_", "-", "_").Replace(key) return strings.ToUpper(replaced) } const generatedHeader = `Code generated by cmd/settingsgen from contracts/settings/v1/manifest.json. DO NOT EDIT. Regenerate with: make settings-bindings Every key, type, scope and default here comes from the manifest, so a client cannot drift from the server's contract by editing a constant. Adding a setting is a manifest change plus a regeneration, never a hand-written key.` func generateGo(contract *settingscontract.Manifest, pkg string) ([]byte, error) { var out bytes.Buffer for _, line := range strings.Split(generatedHeader, "\n") { out.WriteString(strings.TrimRight("// "+line, " ") + "\n") } fmt.Fprintf(&out, "\npackage %s\n\n", pkg) fmt.Fprintf(&out, "// Revision is the manifest revision these bindings were generated from.\nconst Revision = %d\n\n", contract.Revision) out.WriteString("// Setting keys, one constant per definition.\nconst (\n") for _, def := range sortedDefinitions(contract) { fmt.Fprintf(&out, "\t// %s\n", def.Label) fmt.Fprintf(&out, "\t%s = %q\n", identifierFor(def.Key), def.Key) } out.WriteString(")\n\n") out.WriteString("// Remote lists every key the server stores.\nvar Remote = []string{\n") for _, def := range sortedDefinitions(contract) { if def.IsRemote() { fmt.Fprintf(&out, "\t%s,\n", identifierFor(def.Key)) } } out.WriteString("}\n\n") out.WriteString("// ClientLocal lists keys the contract defines but the server never stores.\n") out.WriteString("var ClientLocal = []string{\n") for _, def := range sortedDefinitions(contract) { if !def.IsRemote() { fmt.Fprintf(&out, "\t%s,\n", identifierFor(def.Key)) } } out.WriteString("}\n") return out.Bytes(), nil } func generateTypeScript(contract *settingscontract.Manifest) ([]byte, error) { var out bytes.Buffer out.WriteString("/**\n") for _, line := range strings.Split(generatedHeader, "\n") { out.WriteString(strings.TrimRight(" * "+line, " ") + "\n") } out.WriteString(" */\n\n") fmt.Fprintf(&out, "export const SETTINGS_API_VERSION = %d;\n", contract.APIVersion) fmt.Fprintf(&out, "export const SETTINGS_REVISION = %d;\n\n", contract.Revision) out.WriteString("export interface SettingSuggestedOption {\n") out.WriteString(" value: string;\n") out.WriteString(" introducedIn: number;\n") out.WriteString("}\n\n") out.WriteString("export interface SettingOptionSet {\n") out.WriteString(" type: string;\n") out.WriteString(" options: readonly SettingSuggestedOption[];\n") out.WriteString("}\n\n") out.WriteString("export const SETTING_OPTION_SETS = {\n") for _, name := range sortedOptionSetNames(contract) { optionSet := contract.OptionSets[name] fmt.Fprintf(&out, " %s: {\n", name) fmt.Fprintf(&out, " type: %q,\n", optionSet.Type) out.WriteString(" options: [\n") for _, option := range optionSet.Options { fmt.Fprintf(&out, " { value: %q, introducedIn: %d },\n", option.Value, option.IntroducedIn) } out.WriteString(" ],\n") out.WriteString(" },\n") } out.WriteString("} as const satisfies Record;\n\n") out.WriteString("export type SettingOptionSetId = keyof typeof SETTING_OPTION_SETS;\n\n") out.WriteString("export const SETTING_KEYS = {\n") for _, def := range sortedDefinitions(contract) { fmt.Fprintf(&out, " /** %s */\n", def.Label) fmt.Fprintf(&out, " %s: %q,\n", screamingCase(def.Key), def.Key) } out.WriteString("} as const;\n\n") out.WriteString("export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];\n\n") // The full definition table, so the UI can render controls from the // contract rather than a hand-kept parallel manifest. out.WriteString("export interface SettingDefinition {\n") out.WriteString(" key: SettingKey;\n") out.WriteString(" type: string;\n") out.WriteString(" nullable: boolean;\n") out.WriteString(" persistence: \"remote\" | \"client_local\";\n") out.WriteString(" /** The manifest revision this definition first appeared in. A client\n") out.WriteString(" * pinned to a newer contract than the server's advertised revision must\n") out.WriteString(" * hide definitions, scopes, enum members and widened bounds introduced\n") out.WriteString(" * after that revision — the server would reject them. */\n") out.WriteString(" introducedIn: number;\n") out.WriteString(" scopes: readonly string[];\n") out.WriteString(" /** Revision each scope became writable at, aligned with scopes. */\n") out.WriteString(" scopeIntroducedIn: readonly number[];\n") out.WriteString(" resolutionOrder: readonly string[];\n") out.WriteString(" defaultValue: unknown;\n") out.WriteString(" label: string;\n") out.WriteString(" description: string;\n") out.WriteString(" category: string;\n") out.WriteString(" control?: string;\n") out.WriteString(" unit?: string;\n") out.WriteString(" /** Advisory: platforms this setting is expected to matter on. Absent\n") out.WriteString(" * means everywhere. A client hides settings for platforms it is not,\n") out.WriteString(" * rather than disabling them without explanation. */\n") out.WriteString(" platforms?: readonly string[];\n") out.WriteString(" suggestedOptions?: SettingOptionSetId;\n") out.WriteString(" unsetLabel?: string;\n") out.WriteString(" values?: readonly { value: unknown; label: string; introducedIn: number }[];\n") out.WriteString(" /** Present on enums whose members are ranked, so a ceiling or floor has a direction. */\n") out.WriteString(" ordered?: boolean;\n") out.WriteString(" minimum?: number;\n") out.WriteString(" maximum?: number;\n") out.WriteString(" /** Bound history, oldest first, when a bound was widened after revision 1;\n") out.WriteString(" * a client filtering to an older server revision applies the newest entry\n") out.WriteString(" * whose introducedIn does not exceed it. */\n") out.WriteString(" minimumHistory?: readonly { value: number; introducedIn: number }[];\n") out.WriteString(" maximumHistory?: readonly { value: number; introducedIn: number }[];\n") out.WriteString(" step?: number;\n") out.WriteString(" /** The policy input that narrows this setting, when the manifest binds one. */\n") out.WriteString(" constrainedBy?: {\n") out.WriteString(" policyInput: string;\n") out.WriteString(" constraint: \"ceiling\" | \"floor\" | \"allowlist\" | \"locked\";\n") out.WriteString(" };\n") out.WriteString("}\n\n") out.WriteString("export const SETTING_DEFINITIONS: Record = {\n") for _, def := range sortedDefinitions(contract) { fmt.Fprintf(&out, " %q: {\n", def.Key) fmt.Fprintf(&out, " key: %q,\n", def.Key) fmt.Fprintf(&out, " type: %q,\n", def.ValueSchema.Type) fmt.Fprintf(&out, " nullable: %t,\n", def.ValueSchema.Nullable) fmt.Fprintf(&out, " persistence: %q,\n", def.Persistence) fmt.Fprintf(&out, " introducedIn: %d,\n", def.IntroducedIn) fmt.Fprintf(&out, " scopes: [%s],\n", quotedScopes(def)) fmt.Fprintf(&out, " scopeIntroducedIn: [%s],\n", scopeRevisions(def)) fmt.Fprintf(&out, " resolutionOrder: [%s],\n", quotedResolution(def)) fmt.Fprintf(&out, " defaultValue: %s,\n", defaultLiteral(def)) fmt.Fprintf(&out, " label: %s,\n", jsString(def.Label)) fmt.Fprintf(&out, " description: %s,\n", jsString(def.Description)) fmt.Fprintf(&out, " category: %q,\n", def.Category) if def.Control != "" { fmt.Fprintf(&out, " control: %q,\n", def.Control) } if def.Unit != "" { fmt.Fprintf(&out, " unit: %q,\n", def.Unit) } if len(def.Platforms) > 0 { fmt.Fprintf(&out, " platforms: [%s],\n", quotedStrings(def.Platforms)) } if def.SuggestedOptions != "" { fmt.Fprintf(&out, " suggestedOptions: %q,\n", def.SuggestedOptions) } if def.UnsetLabel != "" { fmt.Fprintf(&out, " unsetLabel: %s,\n", jsString(def.UnsetLabel)) } if len(def.ValueSchema.Values) > 0 { out.WriteString(" values: [\n") for _, member := range def.ValueSchema.Values { encoded, err := json.Marshal(member.Value) if err != nil { return nil, err } fmt.Fprintf(&out, " { value: %s, label: %s, introducedIn: %d },\n", encoded, jsString(member.Label), memberRevision(def, member)) } out.WriteString(" ],\n") } if def.ValueSchema.Ordered { out.WriteString(" ordered: true,\n") } if minimum, ok := def.ValueSchema.Minimum.Current(); ok { fmt.Fprintf(&out, " minimum: %s,\n", trimFloat(minimum)) if history := boundHistory(def, def.ValueSchema.Minimum); history != "" { fmt.Fprintf(&out, " minimumHistory: [%s],\n", history) } } if maximum, ok := def.ValueSchema.Maximum.Current(); ok { fmt.Fprintf(&out, " maximum: %s,\n", trimFloat(maximum)) if history := boundHistory(def, def.ValueSchema.Maximum); history != "" { fmt.Fprintf(&out, " maximumHistory: [%s],\n", history) } } if def.ValueSchema.Step != nil { fmt.Fprintf(&out, " step: %s,\n", trimFloat(*def.ValueSchema.Step)) } if def.ConstrainedBy != nil { fmt.Fprintf(&out, " constrainedBy: { policyInput: %q, constraint: %q },\n", def.ConstrainedBy.PolicyInput, def.ConstrainedBy.Constraint) } out.WriteString(" },\n") } out.WriteString("};\n") return out.Bytes(), nil } func generateKotlin(contract *settingscontract.Manifest, pkg string) ([]byte, error) { var out bytes.Buffer for _, line := range strings.Split(generatedHeader, "\n") { out.WriteString(strings.TrimRight("// "+line, " ") + "\n") } fmt.Fprintf(&out, "\npackage %s\n\n", pkg) out.WriteString("data class SettingSuggestedOption(\n") out.WriteString(" val value: String,\n") out.WriteString(" val introducedIn: Int,\n") out.WriteString(")\n\n") out.WriteString("data class SettingOptionSet(\n") out.WriteString(" val type: String,\n") out.WriteString(" val options: List,\n") out.WriteString(")\n\n") out.WriteString("data class SettingPresentation(\n") out.WriteString(" val suggestedOptions: String? = null,\n") out.WriteString(" val unsetLabel: String? = null,\n") out.WriteString(")\n\n") out.WriteString("object SettingKeys {\n") fmt.Fprintf(&out, " const val REVISION = %d\n\n", contract.Revision) for _, def := range sortedDefinitions(contract) { fmt.Fprintf(&out, " /** %s */\n", def.Label) fmt.Fprintf(&out, " const val %s = %q\n", screamingCase(def.Key), def.Key) } // The allowlist Android maintained by hand, generated instead. The whole // class of "wrote a local key to the server" bug is a manifest question now. out.WriteString("\n /** Every key the server stores. Safe to flush. */\n") out.WriteString(" val REMOTE: List = listOf(\n") for _, def := range sortedDefinitions(contract) { if def.IsRemote() { fmt.Fprintf(&out, " %s,\n", screamingCase(def.Key)) } } out.WriteString(" )\n\n") out.WriteString(" /** Contract-known keys that never leave the device. */\n") out.WriteString(" val CLIENT_LOCAL: List = listOf(\n") for _, def := range sortedDefinitions(contract) { if !def.IsRemote() { fmt.Fprintf(&out, " %s,\n", screamingCase(def.Key)) } } out.WriteString(" )\n") // Type classification, which Android kept as a second hand-maintained table // that had to agree with the first. for _, group := range []struct { name string types []settingscontract.ValueType }{ {"BOOLEAN_KEYS", []settingscontract.ValueType{settingscontract.TypeBoolean}}, {"INT_KEYS", []settingscontract.ValueType{settingscontract.TypeInteger}}, {"DOUBLE_KEYS", []settingscontract.ValueType{settingscontract.TypeNumber}}, } { // Remote only: these tables drive how a value read back from the // server is parsed, and a client_local key never comes back from the // server at all. Listing one would describe a wire format that has no // wire. fmt.Fprintf(&out, "\n val %s: Set = setOf(\n", group.name) for _, def := range sortedDefinitions(contract) { if !def.IsRemote() { continue } for _, want := range group.types { if def.ValueSchema.Type == want { fmt.Fprintf(&out, " %s,\n", screamingCase(def.Key)) } } } out.WriteString(" )\n") } out.WriteString("}\n\n") out.WriteString("object SettingPresentationMetadata {\n") out.WriteString(" val OPTION_SETS: Map = mapOf(\n") for _, name := range sortedOptionSetNames(contract) { optionSet := contract.OptionSets[name] fmt.Fprintf(&out, " %q to SettingOptionSet(\n", name) fmt.Fprintf(&out, " type = %q,\n", optionSet.Type) out.WriteString(" options = listOf(\n") for _, option := range optionSet.Options { fmt.Fprintf(&out, " SettingSuggestedOption(%q, %d),\n", option.Value, option.IntroducedIn) } out.WriteString(" ),\n") out.WriteString(" ),\n") } out.WriteString(" )\n\n") out.WriteString(" val DEFINITIONS: Map = mapOf(\n") for _, def := range sortedDefinitions(contract) { if def.SuggestedOptions == "" && def.UnsetLabel == "" { continue } fmt.Fprintf(&out, " SettingKeys.%s to SettingPresentation(\n", screamingCase(def.Key)) if def.SuggestedOptions != "" { fmt.Fprintf(&out, " suggestedOptions = %q,\n", def.SuggestedOptions) } if def.UnsetLabel != "" { fmt.Fprintf(&out, " unsetLabel = %q,\n", def.UnsetLabel) } out.WriteString(" ),\n") } out.WriteString(" )\n\n") out.WriteString(" fun suggestedValues(key: String, revision: Int = SettingKeys.REVISION): List {\n") out.WriteString(" val setId = DEFINITIONS[key]?.suggestedOptions ?: return emptyList()\n") out.WriteString(" return OPTION_SETS[setId]?.options\n") out.WriteString(" ?.filter { it.introducedIn <= revision }\n") out.WriteString(" ?.map { it.value }\n") out.WriteString(" .orEmpty()\n") out.WriteString(" }\n") out.WriteString("}\n") return out.Bytes(), nil } func generateSwift(contract *settingscontract.Manifest) ([]byte, error) { var out bytes.Buffer for _, line := range strings.Split(generatedHeader, "\n") { out.WriteString(strings.TrimRight("// "+line, " ") + "\n") } out.WriteString("\nimport Foundation\n\n") out.WriteString("public struct SettingSuggestedOption: Hashable, Sendable {\n") out.WriteString(" public let value: String\n") out.WriteString(" public let introducedIn: Int\n") out.WriteString("}\n\n") out.WriteString("public struct SettingOptionSet: Hashable, Sendable {\n") out.WriteString(" public let type: String\n") out.WriteString(" public let options: [SettingSuggestedOption]\n") out.WriteString("}\n\n") out.WriteString("public struct SettingPresentation: Hashable, Sendable {\n") out.WriteString(" public let suggestedOptions: String?\n") out.WriteString(" public let unsetLabel: String?\n") out.WriteString("}\n\n") out.WriteString("/// Every setting the contract defines.\n") out.WriteString("public enum SettingKey: String, CaseIterable, Sendable {\n") for _, def := range sortedDefinitions(contract) { fmt.Fprintf(&out, " /// %s\n", def.Label) fmt.Fprintf(&out, " case %s = %q\n", lowerFirst(identifierFor(def.Key)), def.Key) } out.WriteString("}\n\n") out.WriteString("public extension SettingKey {\n") fmt.Fprintf(&out, " static let revision = %d\n\n", contract.Revision) out.WriteString(" /// Keys the server stores. The rest never leave the device.\n") out.WriteString(" static let remote: [SettingKey] = [\n") for _, def := range sortedDefinitions(contract) { if def.IsRemote() { fmt.Fprintf(&out, " .%s,\n", lowerFirst(identifierFor(def.Key))) } } out.WriteString(" ]\n\n") out.WriteString(" static let clientLocal: [SettingKey] = [\n") for _, def := range sortedDefinitions(contract) { if !def.IsRemote() { fmt.Fprintf(&out, " .%s,\n", lowerFirst(identifierFor(def.Key))) } } out.WriteString(" ]\n") out.WriteString("}\n\n") out.WriteString("public enum SettingPresentationMetadata {\n") out.WriteString(" public static let optionSets: [String: SettingOptionSet] = [\n") for _, name := range sortedOptionSetNames(contract) { optionSet := contract.OptionSets[name] fmt.Fprintf(&out, " %q: SettingOptionSet(\n", name) fmt.Fprintf(&out, " type: %q,\n", optionSet.Type) out.WriteString(" options: [\n") for _, option := range optionSet.Options { fmt.Fprintf(&out, " SettingSuggestedOption(value: %q, introducedIn: %d),\n", option.Value, option.IntroducedIn) } out.WriteString(" ]\n") out.WriteString(" ),\n") } out.WriteString(" ]\n\n") out.WriteString(" public static let definitions: [SettingKey: SettingPresentation] = [\n") for _, def := range sortedDefinitions(contract) { if def.SuggestedOptions == "" && def.UnsetLabel == "" { continue } fmt.Fprintf(&out, " .%s: SettingPresentation(\n", lowerFirst(identifierFor(def.Key))) if def.SuggestedOptions != "" { fmt.Fprintf(&out, " suggestedOptions: %q,\n", def.SuggestedOptions) } else { out.WriteString(" suggestedOptions: nil,\n") } if def.UnsetLabel != "" { fmt.Fprintf(&out, " unsetLabel: %q\n", def.UnsetLabel) } else { out.WriteString(" unsetLabel: nil\n") } out.WriteString(" ),\n") } out.WriteString(" ]\n\n") out.WriteString(" public static func suggestedValues(\n") out.WriteString(" for key: SettingKey,\n") out.WriteString(" revision: Int = SettingKey.revision\n") out.WriteString(" ) -> [String] {\n") out.WriteString(" guard let setID = definitions[key]?.suggestedOptions,\n") out.WriteString(" let optionSet = optionSets[setID] else { return [] }\n") out.WriteString(" return optionSet.options\n") out.WriteString(" .filter { $0.introducedIn <= revision }\n") out.WriteString(" .map(\\.value)\n") out.WriteString(" }\n") out.WriteString("}\n") return out.Bytes(), nil } func lowerFirst(value string) string { if value == "" { return value } return strings.ToLower(value[:1]) + value[1:] } func sortedOptionSetNames(contract *settingscontract.Manifest) []string { names := make([]string, 0, len(contract.OptionSets)) for name := range contract.OptionSets { names = append(names, name) } sort.Strings(names) return names } func quotedStrings(values []string) string { parts := make([]string, 0, len(values)) for _, value := range values { parts = append(parts, fmt.Sprintf("%q", value)) } return strings.Join(parts, ", ") } func quotedScopes(def *settingscontract.Definition) string { parts := make([]string, 0, len(def.AllowedScopes)) for _, entry := range def.AllowedScopes { parts = append(parts, fmt.Sprintf("%q", entry.Scope)) } return strings.Join(parts, ", ") } // scopeRevisions emits each scope's introduction revision, aligned with // quotedScopes. A scope entry with no explicit tag has held since the // definition itself appeared. func scopeRevisions(def *settingscontract.Definition) string { parts := make([]string, 0, len(def.AllowedScopes)) for _, entry := range def.AllowedScopes { revision := entry.IntroducedIn if revision == 0 { revision = def.IntroducedIn } parts = append(parts, fmt.Sprintf("%d", revision)) } return strings.Join(parts, ", ") } // memberRevision is the revision an enum member became a legal value at; an // untagged member has existed since its definition. func memberRevision(def *settingscontract.Definition, member settingscontract.EnumMember) int { if member.IntroducedIn != 0 { return member.IntroducedIn } return def.IntroducedIn } // boundHistory renders a widened bound's full history so an ahead-of-server // client can recover the bound in force at an older revision. Empty when the // bound never changed — the flattened minimum/maximum already carries it. func boundHistory(def *settingscontract.Definition, bound *settingscontract.Bound) string { if bound == nil || len(bound.History) < 2 { return "" } parts := make([]string, 0, len(bound.History)) for _, entry := range bound.History { revision := entry.IntroducedIn if revision == 0 { revision = def.IntroducedIn } parts = append(parts, fmt.Sprintf("{ value: %s, introducedIn: %d }", trimFloat(entry.Value), revision)) } return strings.Join(parts, ", ") } func quotedResolution(def *settingscontract.Definition) string { parts := make([]string, 0, len(def.ResolutionOrder)) for _, scope := range def.ResolutionOrder { parts = append(parts, fmt.Sprintf("%q", scope)) } return strings.Join(parts, ", ") } func defaultLiteral(def *settingscontract.Definition) string { if len(def.DefaultValue) == 0 { return "null" } return string(bytes.TrimSpace(def.DefaultValue)) } func jsString(value string) string { encoded, err := json.Marshal(value) if err != nil { return `""` } return string(encoded) } func trimFloat(value float64) string { return strings.TrimSuffix(fmt.Sprintf("%g", value), ".0") }