* test(web): use safe auth placeholders * feat(settings): sync navigation and card customization * fix(settings): address customization review feedback * fix(settings): address customization review feedback * fix(settings): harden customization capability handling
629 lines
24 KiB
Go
629 lines
24 KiB
Go
// 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 <path> -package org.siloserver.silo.model.settings
|
|
// settingsgen -lang swift -out <path>
|
|
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<string, SettingOptionSet>;\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<SettingKey, SettingDefinition> = {\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<SettingSuggestedOption>,\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<String> = 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<String> = 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<String> = 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<String, SettingOptionSet> = 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<String, SettingPresentation> = 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<String> {\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")
|
|
}
|