Add a codex app-server client with generated types and contract tests
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var clientMethods = []string{
|
||||
"initialize",
|
||||
"thread/start",
|
||||
"thread/resume",
|
||||
"thread/read",
|
||||
"thread/compact/start",
|
||||
"turn/start",
|
||||
"turn/interrupt",
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
any bool
|
||||
Ref string `json:"$ref"`
|
||||
Type json.RawMessage `json:"type"`
|
||||
Enum []json.RawMessage `json:"enum"`
|
||||
Title string `json:"title"`
|
||||
Properties map[string]*schema `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
Items *schema `json:"items"`
|
||||
AdditionalProperties *schema `json:"additionalProperties"`
|
||||
OneOf []*schema `json:"oneOf"`
|
||||
AnyOf []*schema `json:"anyOf"`
|
||||
AllOf []*schema `json:"allOf"`
|
||||
}
|
||||
|
||||
func (s *schema) UnmarshalJSON(b []byte) error {
|
||||
b = bytes.TrimSpace(b)
|
||||
if bytes.Equal(b, []byte("true")) || bytes.Equal(b, []byte("false")) {
|
||||
s.any = true
|
||||
return nil
|
||||
}
|
||||
type plain schema
|
||||
return json.Unmarshal(b, (*plain)(s))
|
||||
}
|
||||
|
||||
func (s *schema) types() []string {
|
||||
if len(s.Type) == 0 {
|
||||
return nil
|
||||
}
|
||||
if s.Type[0] == '"' {
|
||||
t := ""
|
||||
err := json.Unmarshal(s.Type, &t)
|
||||
if err != nil {
|
||||
log.Fatalf("type: %v", err)
|
||||
}
|
||||
return []string{t}
|
||||
}
|
||||
ts := []string{}
|
||||
err := json.Unmarshal(s.Type, &ts)
|
||||
if err != nil {
|
||||
log.Fatalf("type: %v", err)
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
func (s *schema) isNull() bool {
|
||||
ts := s.types()
|
||||
return len(ts) == 1 && ts[0] == "null" && s.Ref == ""
|
||||
}
|
||||
|
||||
type bundle struct {
|
||||
Definitions map[string]json.RawMessage `json:"definitions"`
|
||||
}
|
||||
|
||||
type gen struct {
|
||||
defs map[string]*schema
|
||||
order []string
|
||||
emitted map[string]bool
|
||||
kinds map[string]string
|
||||
out bytes.Buffer
|
||||
consts bytes.Buffer
|
||||
}
|
||||
|
||||
func main() {
|
||||
dir, err := os.MkdirTemp("", "codex-schema")
|
||||
if err != nil {
|
||||
log.Fatalf("mkdir temp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
cmd := exec.Command("codex", "app-server", "generate-json-schema", "--experimental", "--out", dir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Fatalf("generate schema: %v", err)
|
||||
}
|
||||
|
||||
version, err := exec.Command("codex", "--version").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("codex version: %v", err)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "codex_app_server_protocol.schemas.json"))
|
||||
if err != nil {
|
||||
log.Fatalf("read schema: %v", err)
|
||||
}
|
||||
|
||||
b := bundle{}
|
||||
err = json.Unmarshal(raw, &b)
|
||||
if err != nil {
|
||||
log.Fatalf("parse schema: %v", err)
|
||||
}
|
||||
|
||||
g := &gen{defs: map[string]*schema{}, emitted: map[string]bool{}, kinds: map[string]string{}}
|
||||
g.load(b)
|
||||
|
||||
src := g.generate(strings.TrimSpace(string(version)))
|
||||
formatted, err := format.Source(src)
|
||||
if err != nil {
|
||||
os.WriteFile("protocol.gen.go", src, 0o644)
|
||||
log.Fatalf("format: %v", err)
|
||||
}
|
||||
err = os.WriteFile("protocol.gen.go", formatted, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("write: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gen) load(b bundle) {
|
||||
top := map[string]json.RawMessage{}
|
||||
for name, raw := range b.Definitions {
|
||||
if name == "v2" {
|
||||
continue
|
||||
}
|
||||
top[name] = raw
|
||||
}
|
||||
|
||||
v2 := map[string]json.RawMessage{}
|
||||
err := json.Unmarshal(b.Definitions["v2"], &v2)
|
||||
if err != nil {
|
||||
log.Fatalf("parse v2 definitions: %v", err)
|
||||
}
|
||||
|
||||
for _, set := range []map[string]json.RawMessage{top, v2} {
|
||||
for name, raw := range set {
|
||||
s := &schema{}
|
||||
err := json.Unmarshal(raw, s)
|
||||
if err != nil {
|
||||
log.Fatalf("parse %s: %v", name, err)
|
||||
}
|
||||
prev, ok := g.defs[name]
|
||||
if ok {
|
||||
p, q := *prev, *s
|
||||
p.Title, q.Title = "", ""
|
||||
a, _ := json.Marshal(p)
|
||||
c, _ := json.Marshal(q)
|
||||
if !bytes.Equal(a, c) {
|
||||
log.Fatalf("definition %s differs between the top level and v2", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
g.defs[name] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refName(ref string) string {
|
||||
i := strings.LastIndex(ref, "/")
|
||||
return ref[i+1:]
|
||||
}
|
||||
|
||||
type method struct {
|
||||
name string
|
||||
params string
|
||||
response string
|
||||
}
|
||||
|
||||
func (g *gen) variants(def string) []method {
|
||||
s := g.defs[def]
|
||||
if s == nil {
|
||||
log.Fatalf("no definition %s", def)
|
||||
}
|
||||
ms := []method{}
|
||||
for _, v := range s.OneOf {
|
||||
m := v.Properties["method"]
|
||||
if m == nil || len(m.Enum) != 1 {
|
||||
log.Fatalf("%s variant without a single method", def)
|
||||
}
|
||||
name := ""
|
||||
err := json.Unmarshal(m.Enum[0], &name)
|
||||
if err != nil {
|
||||
log.Fatalf("%s method: %v", def, err)
|
||||
}
|
||||
params := ""
|
||||
if p := v.Properties["params"]; p != nil && p.Ref != "" {
|
||||
params = refName(p.Ref)
|
||||
}
|
||||
response := ""
|
||||
if strings.HasSuffix(params, "Params") {
|
||||
r := strings.TrimSuffix(params, "Params") + "Response"
|
||||
if g.defs[r] != nil {
|
||||
response = r
|
||||
}
|
||||
}
|
||||
ms = append(ms, method{name: name, params: params, response: response})
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
func (g *gen) generate(version string) []byte {
|
||||
client := []method{}
|
||||
for _, m := range g.variants("ClientRequest") {
|
||||
if slices.Contains(clientMethods, m.name) {
|
||||
client = append(client, m)
|
||||
}
|
||||
}
|
||||
if len(client) != len(clientMethods) {
|
||||
log.Fatalf("found %d of %d client methods", len(client), len(clientMethods))
|
||||
}
|
||||
notifications := g.variants("ServerNotification")
|
||||
requests := g.variants("ServerRequest")
|
||||
|
||||
roots := []string{}
|
||||
for _, set := range [][]method{client, notifications, requests} {
|
||||
for _, m := range set {
|
||||
if m.params != "" {
|
||||
roots = append(roots, m.params)
|
||||
}
|
||||
if m.response != "" {
|
||||
roots = append(roots, m.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, r := range roots {
|
||||
g.named(r)
|
||||
}
|
||||
|
||||
h := bytes.Buffer{}
|
||||
fmt.Fprintf(&h, "// Code generated by go run ./gen from the %s app-server schema. DO NOT EDIT.\n\n", version)
|
||||
h.WriteString("package codex\n\nimport \"encoding/json\"\n\n")
|
||||
fmt.Fprintf(&h, "const SchemaVersion = %q\n\n", version)
|
||||
|
||||
h.WriteString("const (\n")
|
||||
seen := map[string]bool{}
|
||||
for _, set := range [][]method{client, notifications, requests} {
|
||||
for _, m := range set {
|
||||
c := "Method" + pascal(m.name)
|
||||
if seen[c] {
|
||||
continue
|
||||
}
|
||||
seen[c] = true
|
||||
fmt.Fprintf(&h, "%s = %q\n", c, m.name)
|
||||
}
|
||||
}
|
||||
h.WriteString(")\n\n")
|
||||
|
||||
writeTable := func(name string, ms []method, field func(method) string) {
|
||||
fmt.Fprintf(&h, "var %s = map[string]func() any{\n", name)
|
||||
for _, m := range ms {
|
||||
t := field(m)
|
||||
if t == "" {
|
||||
fmt.Fprintf(&h, "Method%s: nil,\n", pascal(m.name))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&h, "Method%s: func() any { return &%s{} },\n", pascal(m.name), t)
|
||||
}
|
||||
h.WriteString("}\n\n")
|
||||
}
|
||||
writeTable("clientResults", client, func(m method) string { return m.response })
|
||||
writeTable("notificationParams", notifications, func(m method) string { return m.params })
|
||||
writeTable("serverRequestParams", requests, func(m method) string { return m.params })
|
||||
writeTable("serverRequestResponses", requests, func(m method) string { return m.response })
|
||||
|
||||
h.Write(g.consts.Bytes())
|
||||
h.Write(g.out.Bytes())
|
||||
return h.Bytes()
|
||||
}
|
||||
|
||||
func (g *gen) named(name string) string {
|
||||
s := g.defs[name]
|
||||
if s == nil {
|
||||
log.Fatalf("no definition %s", name)
|
||||
}
|
||||
if g.emitted[name] {
|
||||
return name
|
||||
}
|
||||
g.emitted[name] = true
|
||||
g.kinds[name] = g.kindOf(s)
|
||||
g.emitType(name, s)
|
||||
return name
|
||||
}
|
||||
|
||||
func (g *gen) resolve(s *schema) *schema {
|
||||
for s.Ref != "" {
|
||||
s = g.defs[refName(s.Ref)]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func nonNull(list []*schema) []*schema {
|
||||
out := []*schema{}
|
||||
for _, v := range list {
|
||||
if !v.isNull() {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *gen) kindOf(s *schema) string {
|
||||
if s.any {
|
||||
return "raw"
|
||||
}
|
||||
if s.Ref != "" {
|
||||
return g.kindOf(g.resolve(s))
|
||||
}
|
||||
if len(s.AllOf) == 1 {
|
||||
return g.kindOf(s.AllOf[0])
|
||||
}
|
||||
alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...))
|
||||
if len(s.Properties) == 0 && len(alts) == 1 {
|
||||
return g.kindOf(alts[0])
|
||||
}
|
||||
if len(s.Properties) == 0 && len(alts) > 1 {
|
||||
kinds := map[string]bool{}
|
||||
for _, a := range alts {
|
||||
kinds[g.kindOf(a)] = true
|
||||
}
|
||||
if len(kinds) == 1 && (kinds["string"] || kinds["struct"]) {
|
||||
for k := range kinds {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return "raw"
|
||||
}
|
||||
if len(s.Properties) > 0 {
|
||||
return "struct"
|
||||
}
|
||||
ts := []string{}
|
||||
for _, t := range s.types() {
|
||||
if t != "null" {
|
||||
ts = append(ts, t)
|
||||
}
|
||||
}
|
||||
if len(ts) != 1 {
|
||||
return "raw"
|
||||
}
|
||||
switch ts[0] {
|
||||
case "string":
|
||||
return "string"
|
||||
case "integer", "number", "boolean":
|
||||
return "scalar"
|
||||
case "array":
|
||||
return "slice"
|
||||
case "object":
|
||||
return "map"
|
||||
}
|
||||
return "raw"
|
||||
}
|
||||
|
||||
func (g *gen) emitType(name string, s *schema) {
|
||||
switch g.kindOf(s) {
|
||||
case "struct":
|
||||
fields := g.collectFields(s)
|
||||
g.emitStruct(name, fields)
|
||||
g.emitDiscriminators(name, s)
|
||||
case "string":
|
||||
fmt.Fprintf(&g.out, "type %s string\n\n", name)
|
||||
values := g.enumValues(s)
|
||||
if len(values) > 0 {
|
||||
g.consts.WriteString("const (\n")
|
||||
for _, v := range values {
|
||||
fmt.Fprintf(&g.consts, "%s%s %s = %q\n", name, pascal(v), name, v)
|
||||
}
|
||||
g.consts.WriteString(")\n\n")
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(&g.out, "type %s = %s\n\n", name, g.goType(s, name))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gen) enumValues(s *schema) []string {
|
||||
if s.Ref != "" {
|
||||
return g.enumValues(g.resolve(s))
|
||||
}
|
||||
out := []string{}
|
||||
for _, e := range s.Enum {
|
||||
v := ""
|
||||
if json.Unmarshal(e, &v) == nil {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
for _, a := range append(append(append([]*schema{}, s.OneOf...), s.AnyOf...), s.AllOf...) {
|
||||
for _, v := range g.enumValues(a) {
|
||||
if !slices.Contains(out, v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type field struct {
|
||||
json string
|
||||
schemas []*schema
|
||||
required int
|
||||
seen int
|
||||
}
|
||||
|
||||
type objectAlt struct {
|
||||
props map[string]*schema
|
||||
required []string
|
||||
}
|
||||
|
||||
func (g *gen) objectAlts(s *schema) []objectAlt {
|
||||
if s.Ref != "" {
|
||||
return g.objectAlts(g.resolve(s))
|
||||
}
|
||||
if len(s.AllOf) == 1 && len(s.Properties) == 0 {
|
||||
return g.objectAlts(s.AllOf[0])
|
||||
}
|
||||
alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...))
|
||||
base := objectAlt{props: s.Properties, required: s.Required}
|
||||
if len(alts) == 0 {
|
||||
return []objectAlt{base}
|
||||
}
|
||||
out := []objectAlt{}
|
||||
for _, a := range alts {
|
||||
for _, sub := range g.objectAlts(a) {
|
||||
props := map[string]*schema{}
|
||||
for k, v := range base.props {
|
||||
props[k] = v
|
||||
}
|
||||
for k, v := range sub.props {
|
||||
props[k] = v
|
||||
}
|
||||
out = append(out, objectAlt{props: props, required: append(append([]string{}, base.required...), sub.required...)})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *gen) collectFields(s *schema) []*field {
|
||||
alts := g.objectAlts(s)
|
||||
byName := map[string]*field{}
|
||||
names := []string{}
|
||||
for _, a := range alts {
|
||||
for k, v := range a.props {
|
||||
f := byName[k]
|
||||
if f == nil {
|
||||
f = &field{json: k}
|
||||
byName[k] = f
|
||||
names = append(names, k)
|
||||
}
|
||||
f.schemas = append(f.schemas, v)
|
||||
f.seen++
|
||||
if slices.Contains(a.required, k) {
|
||||
f.required++
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
out := []*field{}
|
||||
for _, n := range names {
|
||||
f := byName[n]
|
||||
if f.seen < len(alts) {
|
||||
f.required = 0
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *gen) emitStruct(name string, fields []*field) {
|
||||
body := bytes.Buffer{}
|
||||
used := map[string]bool{}
|
||||
for _, f := range fields {
|
||||
goName := fieldName(f.json)
|
||||
for used[goName] {
|
||||
goName += "_"
|
||||
}
|
||||
used[goName] = true
|
||||
|
||||
types := map[string]bool{}
|
||||
nullable := false
|
||||
var first *schema
|
||||
for _, fs := range f.schemas {
|
||||
t, n := g.fieldType(fs, name+goName)
|
||||
types[t] = true
|
||||
nullable = nullable || n
|
||||
if first == nil {
|
||||
first = fs
|
||||
}
|
||||
}
|
||||
t := ""
|
||||
if len(types) == 1 {
|
||||
for k := range types {
|
||||
t = k
|
||||
}
|
||||
} else {
|
||||
t = "json.RawMessage"
|
||||
}
|
||||
|
||||
required := f.required > 0 && f.required == f.seen && !nullable
|
||||
if !required && g.kindOf(first) == "struct" && len(types) == 1 && !strings.HasPrefix(t, "*") {
|
||||
t = "*" + t
|
||||
}
|
||||
tag := f.json
|
||||
if !required {
|
||||
tag += ",omitempty"
|
||||
}
|
||||
fmt.Fprintf(&body, "%s %s `json:%q`\n", goName, t, tag)
|
||||
}
|
||||
fmt.Fprintf(&g.out, "type %s struct {\n%s}\n\n", name, body.String())
|
||||
}
|
||||
|
||||
func (g *gen) emitDiscriminators(name string, s *schema) {
|
||||
r := g.resolve(s)
|
||||
alts := nonNull(append(append([]*schema{}, r.OneOf...), r.AnyOf...))
|
||||
if len(alts) < 2 {
|
||||
return
|
||||
}
|
||||
values := []string{}
|
||||
for _, a := range alts {
|
||||
a = g.resolve(a)
|
||||
t := a.Properties["type"]
|
||||
if t == nil || len(t.Enum) != 1 {
|
||||
return
|
||||
}
|
||||
v := ""
|
||||
if json.Unmarshal(t.Enum[0], &v) != nil {
|
||||
return
|
||||
}
|
||||
values = append(values, v)
|
||||
}
|
||||
g.consts.WriteString("const (\n")
|
||||
for _, v := range values {
|
||||
fmt.Fprintf(&g.consts, "%sType%s = %q\n", name, pascal(v), v)
|
||||
}
|
||||
g.consts.WriteString(")\n\n")
|
||||
}
|
||||
|
||||
func (g *gen) fieldType(s *schema, ctx string) (string, bool) {
|
||||
if s.any {
|
||||
return "json.RawMessage", false
|
||||
}
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref)), false
|
||||
}
|
||||
if len(s.AllOf) == 1 && len(s.Properties) == 0 {
|
||||
return g.fieldType(s.AllOf[0], ctx)
|
||||
}
|
||||
all := append(append([]*schema{}, s.OneOf...), s.AnyOf...)
|
||||
alts := nonNull(all)
|
||||
nullable := len(alts) < len(all)
|
||||
if len(s.Properties) == 0 && len(alts) == 1 {
|
||||
t, n := g.fieldType(alts[0], ctx)
|
||||
return t, n || nullable
|
||||
}
|
||||
ts := s.types()
|
||||
if slices.Contains(ts, "null") {
|
||||
nullable = true
|
||||
}
|
||||
return g.goType(s, ctx), nullable
|
||||
}
|
||||
|
||||
func (g *gen) goType(s *schema, ctx string) string {
|
||||
switch g.kindOf(s) {
|
||||
case "raw":
|
||||
return "json.RawMessage"
|
||||
case "struct":
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
if !g.emitted[ctx] {
|
||||
g.emitted[ctx] = true
|
||||
g.emitStruct(ctx, g.collectFields(s))
|
||||
g.emitDiscriminators(ctx, s)
|
||||
}
|
||||
return ctx
|
||||
case "string":
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...))
|
||||
if len(alts) == 1 {
|
||||
return g.goType(alts[0], ctx)
|
||||
}
|
||||
return "string"
|
||||
case "scalar":
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
for _, t := range s.types() {
|
||||
switch t {
|
||||
case "integer":
|
||||
return "int64"
|
||||
case "number":
|
||||
return "float64"
|
||||
case "boolean":
|
||||
return "bool"
|
||||
}
|
||||
}
|
||||
case "slice":
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
if s.Items == nil {
|
||||
return "[]json.RawMessage"
|
||||
}
|
||||
t, _ := g.fieldType(s.Items, ctx+"Item")
|
||||
return "[]" + t
|
||||
case "map":
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
if s.AdditionalProperties == nil || s.AdditionalProperties.any {
|
||||
return "map[string]any"
|
||||
}
|
||||
t, _ := g.fieldType(s.AdditionalProperties, ctx+"Value")
|
||||
return "map[string]" + t
|
||||
}
|
||||
if s.Ref != "" {
|
||||
return g.named(refName(s.Ref))
|
||||
}
|
||||
alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...))
|
||||
if len(alts) == 1 {
|
||||
return g.goType(alts[0], ctx)
|
||||
}
|
||||
return "json.RawMessage"
|
||||
}
|
||||
|
||||
func pascal(s string) string {
|
||||
b := strings.Builder{}
|
||||
up := true
|
||||
for _, r := range s {
|
||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
||||
up = true
|
||||
continue
|
||||
}
|
||||
if up {
|
||||
b.WriteRune(unicode.ToUpper(r))
|
||||
up = false
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
out := b.String()
|
||||
if out == "" || unicode.IsDigit(rune(out[0])) {
|
||||
out = "X" + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fieldName(s string) string {
|
||||
p := []rune(pascal(s))
|
||||
out := []rune{}
|
||||
for i := 0; i < len(p); i++ {
|
||||
end := func(n int) bool { return i+n == len(p) || unicode.IsUpper(p[i+n]) || unicode.IsDigit(p[i+n]) }
|
||||
if i+1 < len(p) && p[i] == 'I' && p[i+1] == 'd' && end(2) {
|
||||
out = append(out, 'I', 'D')
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if i+2 < len(p) && p[i] == 'I' && p[i+1] == 'd' && p[i+2] == 's' && end(3) {
|
||||
out = append(out, 'I', 'D', 's')
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if i+2 < len(p) && p[i] == 'U' && p[i+1] == 'r' && p[i+2] == 'l' && end(3) {
|
||||
out = append(out, 'U', 'R', 'L')
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
out = append(out, p[i])
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Reference in New Issue
Block a user