okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f52e1c5
main
1// apigen derives Go wire types and HTTP requests from the daemon contract. 2package main 3 4import ( 5"bytes" 6"encoding/json" 7"flag" 8"fmt" 9"maps" 10"os" 11"path/filepath" 12"strings" 13 14"github.com/getkin/kin-openapi/openapi3" 15"github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen" 16"go.yaml.in/yaml/v3" 17) 18 19func main () { 20check := flag .Bool ("check" ,false ,"fail if generated code differs" ) 21flag .Parse () 22if err := generate (* check );err != nil { 23fmt .Fprintln (os .Stderr ,err ) 24os .Exit (1 ) 25 } 26} 27 28func generate (check bool )error { 29root := filepath .Join (".." ,".." ,".." ) 30data ,err := os .ReadFile (filepath .Join (root ,"docs" ,"openapi.yaml" )) 31if err != nil { 32return err 33 } 34var document map [string ]any 35if err := yaml .Unmarshal (data ,& document );err != nil { 36return err 37 } 38// oapi-codegen supports 3.1. SSE item schemas stay in the canonical 3.2 39// document; the adapter owns framing, delivery and cursor recovery. 40document ["openapi" ]= "3.1.0" 41schemas := document ["components" ].(map [string ]any )["schemas" ].(map [string ]any ) 42for name ,schema := range schemas { 43project (schema ) 44if name == "DynamicJSON" { 45schema .(map [string ]any )["x-go-type" ]= "json.RawMessage" 46 } 47 } 48projectParameters (document ["paths" ]) 49projectParameters (document ["components" ].(map [string ]any )["parameters" ]) 50data ,err = json .Marshal (document ) 51if err != nil { 52return err 53 } 54spec ,err := openapi3 .NewLoader ().LoadFromData (data ) 55if err != nil { 56return err 57 } 58version := "v2.8.0" 59source ,err := codegen .Generate (spec , codegen.Configuration { 60PackageName :"protocol" , 61Generate : codegen.GenerateOptions {Models :true ,Client :true }, 62Compatibility : codegen.CompatibilityOptions {DisableRequiredReadOnlyAsPointer :true }, 63OutputOptions : codegen.OutputOptions { 64SkipPrune :true , 65SkipEnumValidate :true , 66NameNormalizer :"ToCamelCaseWithInitialisms" , 67AdditionalInitialisms : []string {"UTF8" ,"TTL" ,"MCP" ,"HTTP" ,"JJ" }, 68UserTemplates :map [string ]string {"client-with-responses.tmpl" :"// Response decoding is owned by the daemon adapter.\n" }, 69 }, 70NoVCSVersionOverride :& version , 71 }) 72if err != nil { 73return err 74 } 75target := filepath .Join (root ,"cli" ,"internal" ,"daemon" ,"protocol" ,"api.gen.go" ) 76if check { 77current ,err := os .ReadFile (target ) 78if err != nil { 79return err 80 } 81if ! bytes .Equal (current , []byte (source )) { 82return fmt .Errorf ("generated API is stale; run go generate ./internal/daemon" ) 83 } 84return nil 85 } 86return os .WriteFile (target , []byte (source ),0644 ) 87} 88 89// project selects Go representations without changing the wire contract. 90// Unions remain raw JSON for the adapter's discriminated decoding. Scalars 91// keep their wire values; timestamps are parsed by the application adapter. 92func project (value any ) { 93switch node := value .(type ) { 94case []any : 95for _ ,child := range node { 96project (child ) 97 } 98case map [string ]any : 99for _ ,key := range []string {"properties" ,"$defs" ,"items" ,"prefixItems" ,"anyOf" ,"oneOf" ,"allOf" ,"additionalProperties" } { 100child := node [key ] 101if key == "properties" || key == "$defs" { 102if members ,ok := child .(map [string ]any );ok { 103for _ ,member := range members { 104project (member ) 105 } 106 } 107 }else { 108project (child ) 109 } 110 } 111if properties ,ok := node ["properties" ].(map [string ]any );ok { 112for name ,child := range properties { 113property := child .(map [string ]any ) 114if name == "etag" { 115property ["x-go-name" ]= "ETag" 116 } 117if baseName ,hasIDsSuffix := strings .CutSuffix (name ,"_ids" );hasIDsSuffix { 118property ["x-go-name" ]= codegen .ToCamelCaseWithInitialisms (baseName )+ "IDs" 119 } 120kind ,_ := property ["type" ].(string ) 121if kind == "array" || property ["x-go-type" ]== "json.RawMessage" || (kind == "object" && property ["properties" ]== nil ) { 122property ["x-go-type-skip-optional-pointer" ]= true 123 } 124 } 125 } 126if items ,ok := node ["items" ].(bool );ok && ! items { 127// Object-form false schema keeps the tuple restriction while 128// avoiding the loader's unsupported boolean items schema. 129node ["items" ]= map [string ]any {"not" :map [string ]any {}} 130 } 131if constant ,ok := node ["const" ];ok { 132switch constant .(type ) { 133case string : 134node ["type" ]= "string" 135case int ,int64 : 136node ["type" ]= "integer" 137case bool : 138node ["type" ]= "boolean" 139 } 140 } 141kind ,_ := node ["type" ].(string ) 142nullable := kind == "null" 143if kinds ,ok := node ["type" ].([]any );ok { 144for _ ,member := range kinds { 145if member == "null" { 146nullable = true 147 } 148 } 149if len (kinds )== 2 && nullable { 150for _ ,member := range kinds { 151if member != "null" { 152kind = member .(string ) 153 } 154 } 155 }else { 156node ["x-go-type" ]= "json.RawMessage" 157 } 158 } 159for _ ,keyword := range []string {"anyOf" ,"oneOf" } { 160members ,ok := node [keyword ].([]any ) 161if ! ok { 162continue 163 } 164var nonnull []any 165for _ ,member := range members { 166if member .(map [string ]any )["type" ]== "null" { 167nullable = true 168 }else { 169nonnull = append (nonnull ,member ) 170 } 171 } 172if len (nonnull )== 1 && nullable && nonnull [0 ].(map [string ]any )["type" ]== "array" { 173// Inline nullable arrays otherwise lose their array shape in 174// oapi-codegen's single-branch union handling. 175maps .Copy (node ,nonnull [0 ].(map [string ]any )) 176node ["type" ]= []any {"array" ,"null" } 177delete (node ,keyword ) 178kind = "array" 179 }else if node ["properties" ]!= nil { 180delete (node ,keyword ) 181 }else if len (nonnull )!= 1 || ! nullable { 182node ["x-go-type" ]= "json.RawMessage" 183 } 184 } 185switch kind { 186case "string" : 187node ["x-go-type" ]= "string" 188case "integer" : 189node ["x-go-type" ]= "int64" 190case "number" : 191node ["x-go-type" ]= "float64" 192case "boolean" : 193node ["x-go-type" ]= "bool" 194 } 195if node ["x-go-type" ]== "json.RawMessage" { 196delete (node ,"anyOf" ) 197delete (node ,"oneOf" ) 198delete (node ,"type" ) 199node ["x-go-type-skip-optional-pointer" ]= true 200 } 201if kind == "array" { 202node ["x-go-type-skip-optional-pointer" ]= true 203 } 204if nullable { 205node ["x-oapi-codegen-extra-tags" ]= map [string ]any {"nullable" :"true" } 206 } 207if len (node )== 0 { 208node ["x-go-type" ]= "json.RawMessage" 209 } 210 211 } 212} 213 214func projectParameters (value any ) { 215switch node := value .(type ) { 216case []any : 217for _ ,child := range node { 218projectParameters (child ) 219 } 220case map [string ]any : 221for key ,child := range node { 222if key == "schema" || key == "itemSchema" { 223project (child ) 224 }else if key != "examples" && key != "example" { 225projectParameters (child ) 226 } 227 } 228 } 229}