Archived
1
0
Fork 0
This repository has been archived on 2026-01-19. You can view files and clone it, but cannot push or open issues or pull requests.
Danmaku/client/codegen/main.go

639 lines
18 KiB
Go

// Copyright 2018 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"text/template"
)
var utilities = map[string]string {
"ApiAccount":
`
var _wallet_dict = null
var wallet_dict : Dictionary:
get:
if _wallet_dict == null:
if _wallet == null:
return {}
var json = JSON.new()
if json.parse(_wallet) != OK:
return {}
_wallet_dict = json.get_data()
return _wallet_dict as Dictionary
`,
}
const codeTemplate string = `### Code generated by codegen/main.go. DO NOT EDIT. ###
extends RefCounted
class_name NakamaAPI
{{- range $defname, $definition := .Definitions }}
{{- $classname := $defname | title }}
{{- if isRefToEnum $classname }}
# {{ enumSummary $definition | stripNewlines }}
{{- range $idx, $val := ($definition | enumDescriptions) }}
# {{ $val }}
{{- end -}}
# {{ $definition | enumDescriptions }}
enum {{ $classname | title }} { {{- range $idx, $enum := $definition.Enum }}{{ $enum }} = {{ $idx }},{{- end -}} }
{{- else }}
# {{ $definition.Description | stripNewlines }}
class {{ $classname }} extends NakamaAsyncResult:
const _SCHEMA = {
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
"{{ $fieldname }}": {"name": "{{ $_field }}", "type": {{ $gdType | godotSchemaType }}, "required": false
{{- if eq $property.Type "array" -}}
, "content": {{ (godotType $property.Items.Type $property.Items.Ref "" "" false) | godotSchemaType }}
{{- else if eq $property.Type "object" -}}
, "content": {{ (godotType $property.AdditionalProperties.Type "" "" "" false) | godotSchemaType }}
{{- end -}}
},
{{- end }}
}
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
{{- $gdDef := $gdType | godotDef }}
# {{ $property.Description }}
var {{ $_field }}
var {{ $fieldname }} : {{ $gdType }}:
get:
{{- if $property.Ref }}
{{- if isRefToEnum (cleanRef $property.Ref) }}{{/* Enums */}}
return {{ cleanRef $property.Ref }}.values()[0] if not {{ cleanRef $property.Ref }}.values().has({{ $_field }}) else {{ $_field }}
{{- else }}{{/* Object reference */}}
return _{{ $fieldname }} as {{ $gdType }}
{{- end }}
{{- else if eq $property.Type "object"}}{{/* Dictionaries */}}
return Dictionary() if not {{ $_field }} is Dictionary else {{ $_field }}.duplicate()
{{- else }}{{/* Simple type */}}
return {{ $gdDef }} if not {{ $_field }} is {{ $gdType }} else {{ $gdType }}({{ $_field }})
{{- end }}
{{- end }}
{{- godotClassUtils $classname }}
func _init(p_exception = null):
super(p_exception)
static func create(p_ns : GDScript, p_dict : Dictionary) -> {{ $classname }}:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "{{ $classname }}", p_dict), {{ $classname }}) as {{ $classname }}
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string() -> String:
if is_exception():
return get_exception()._to_string()
var output : String = ""
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- if eq $property.Type "array" }}
output += "{{ $fieldname }}: %s, " % [{{ $_field }}]
{{- else if eq $property.Type "object" }}
var map_string : String = ""
if typeof({{ $_field }}) == TYPE_DICTIONARY:
for k in {{ $_field }}:
map_string += "{%s=%s}, " % [k, {{ $_field }}[k]]
output += "{{ $fieldname }}: [%s], " % map_string
{{- else }}
output += "{{ $fieldname }}: %s, " % {{ $_field }}
{{- end }}
{{- end }}
return output
{{- end }}
{{- end }}
# The low level client for the Nakama API.
class ApiClient extends RefCounted:
var _base_uri : String
var _http_adapter
var _namespace : GDScript
var _server_key : String
var auto_refresh := true
var auto_refresh_time := 300
var auto_retry : bool:
set(p_value):
_http_adapter.auto_retry = p_value
get:
return _http_adapter.auto_retry
var auto_retry_count : int:
set(p_value):
_http_adapter.auto_retry_count = p_value
get:
return _http_adapter.auto_retry_count
var auto_retry_backoff_base : int:
set(p_value):
_http_adapter.auto_retry_backoff_base = p_value
get:
return _http_adapter.auto_retry_backoff_base
var last_cancel_token:
get:
return _http_adapter.get_last_token()
func _init(p_base_uri : String, p_http_adapter, p_namespace : GDScript, p_server_key : String, p_timeout : int = 10):
_base_uri = p_base_uri
_http_adapter = p_http_adapter
_http_adapter.timeout = p_timeout
_namespace = p_namespace
_server_key = p_server_key
func _refresh_session(p_session : NakamaSession):
if auto_refresh and p_session.is_valid() and p_session.refresh_token and not p_session.is_refresh_expired() and p_session.would_expire_in(auto_refresh_time):
var request = ApiSessionRefreshRequest.new()
request._token = p_session.refresh_token
return await session_refresh_async(_server_key, "", request)
return null
func cancel_request(p_token):
if p_token:
_http_adapter.cancel_request(p_token)
{{- range $url, $path := .Paths }}
{{- range $method, $operation := $path}}
# {{ $operation.Summary | stripNewlines }}
{{- if $operation.Responses.Ok.Schema.Ref }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- else }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- end}}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
p_basic_auth_username : String
, p_basic_auth_password : String
{{- else if eq $key "HttpKeyAuth" }}
p_bearer_token : String
{{- end }}
{{- end }}
{{- end }}
{{- else }}
p_session : NakamaSession
{{- end }}
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if not $parameter.Required }}{{/* Godot does not support typed optional parameters yet. */}}
, {{ $argument }} = null # : {{ $parameter.Type }}
{{- else if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
, {{ $argument }} : String
{{- else }}
, {{ $argument }} : {{ $parameter.Schema.Ref | cleanRef }}
{{- end }}
{{- else }}
, {{ $argument }} : {{ godotType $parameter.Type $parameter.Schema.Ref $parameter.Items.Type "" (isRefToEnum (cleanRef $parameter.Schema.Ref)) }}
{{- end }}
{{- end }}
)
{{- if $operation.Responses.Ok.Schema.Ref }} -> {{ $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- else }} -> NakamaAsyncResult
{{- end }}:
{{- $classname := "NakamaAsyncResult" }}
{{- if $operation.Responses.Ok.Schema.Ref }}
{{- $classname = $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- end }}
{{- if not $operation.Security }}
var try_refresh = await _refresh_session(p_session)
if try_refresh != null:
if try_refresh.is_exception():
return {{ $classname }}.new(try_refresh.get_exception())
await p_session.refresh(try_refresh)
{{- end }}
var urlpath : String = "{{- $url }}"
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "path" }}
urlpath = urlpath.replace("{{- print "{" $parameter.Name "}"}}", NakamaSerializer.escape_http({{ $argument }}))
{{- end }}
{{- end }}
var query_params = ""
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- $snakecase := $parameter.Name | pascalToSnake }}
{{- if eq $parameter.In "query"}}
{{- if $parameter.Required }}
if true: # Hack for static checks
{{- else }}
if {{ $argument }} != null:
{{- end }}
{{- if eq $parameter.Type "integer" }}
query_params += "{{- $snakecase }}=%d&" % {{ $argument }}
{{- else if eq $parameter.Type "string" }}
query_params += "{{- $snakecase }}=%s&" % NakamaSerializer.escape_http({{ $argument }})
{{- else if eq $parameter.Type "boolean" }}
query_params += "{{- $snakecase }}=%s&" % str(bool({{ $argument }})).to_lower()
{{- else if eq $parameter.Type "array" }}
for elem in {{ $argument }}:
query_params += "{{- $snakecase }}=%s&" % elem
{{- else }}
{{ $parameter }} // ERROR
{{- end }}
{{- end }}
{{- end }}
var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""]
var method = "{{- $method | uppercase }}"
var headers = {}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password)
var header = "Basic %s" % credentials
headers["Authorization"] = header
{{- else if eq $key "HttpKeyAuth" }}
if (p_bearer_token):
var header = "Bearer %s" % p_bearer_token
headers["Authorization"] = header
{{- end }}
{{- end }}
{{- end }}
{{- else }}
var header = "Bearer %s" % p_session.token
headers["Authorization"] = header
{{- end }}
var content : PackedByteArray
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
content = JSON.stringify({{ $argument }}).to_utf8_buffer()
{{- else }}
content = JSON.stringify({{ $argument }}.serialize()).to_utf8_buffer()
{{- end }}
{{- end }}
{{- end }}
var result = await _http_adapter.send_async(method, uri, headers, content)
if result is NakamaException:
return {{ $classname }}.new(result)
{{- if $operation.Responses.Ok.Schema.Ref }}
var out : {{ $classname }} = NakamaSerializer.deserialize(_namespace, "{{ $classname }}", result)
return out
{{- else }}
return NakamaAsyncResult.new()
{{- end}}
{{- end }}
{{- end }}
`
func convertRefToClassName(input string) (className string) {
cleanRef := strings.TrimPrefix(input, "#/definitions/")
className = strings.Title(cleanRef)
return
}
func stripNewlines(input string) (output string) {
output = strings.Replace(input, "\n", " ", -1)
return
}
func prependParameter(input string) (output string) {
output = "p_" + pascalToSnake(input)
return
}
func pascalToSnake(input string) (output string) {
output = ""
prev_low := false
for _, v := range input {
is_cap := v >= 'A' && v <= 'Z'
is_low := v >= 'a' && v <= 'z'
if is_cap && prev_low {
output = output + "_"
}
output += strings.ToLower(string(v))
prev_low = is_low
}
return
}
func apiFuncName(input string) (output string) {
output = pascalToSnake(input[7:])
return
}
func godotType(p_type string, p_ref string, p_item_type string, p_extra string, p_is_enum bool) (out string) {
is_array := false
is_dict := false
switch p_type {
case "integer":
out = "int"
case "string":
out = "String"
case "boolean":
out = "bool"
case "array":
is_array = true
case "object":
is_dict = true
default:
if p_is_enum {
out = "int"
} else {
out = convertRefToClassName(p_ref)
}
}
if is_array {
switch p_item_type {
case "integer":
out = "PackedIntArray"
return
case "string":
out = "PackedStringArray"
return
case "boolean":
out = "PackedIntArray"
return
default:
out = "Array"
}
}
if is_dict {
out = "Dictionary"
}
return
}
func godotDef(p_type string) (out string) {
switch(p_type) {
case "bool": out = "false"
case "int": out = "0"
case "String": out = "\"\""
case "PackedIntArray": out = "PackedIntArray()"
case "PackedStringArray": out = "PackedStringArray()"
case "Array": out = "Array()"
case "Dictionary": out = "Dictionary()"
}
return
}
func godotLooseType(p_type string) (out string) {
switch(p_type) {
case "PackedStringArray", "PackedIntArray":
out = "Array"
default:
out = p_type
}
return
}
func godotSchemaType(p_type string) (out string) {
out = "TYPE_"
switch(p_type) {
case "bool": out += "BOOL"
case "int": out += "INT"
case "String": out += "STRING"
case "PackedIntArray": out += "ARRAY"
case "PackedStringArray": out += "ARRAY"
case "Array": out += "ARRAY"
case "Dictionary": out += "DICTIONARY"
default: out = "\"" + p_type + "\""
}
return
}
func pascalToCamel(input string) (camelCase string) {
if input == "" {
return ""
}
camelCase = strings.ToLower(string(input[0]))
camelCase += string(input[1:])
return camelCase
}
func camelToPascal(camelCase string) (pascalCase string) {
if len(camelCase) <= 0 {
return ""
}
pascalCase = strings.ToUpper(string(camelCase[0])) + camelCase[1:]
return
}
func enumSummary(def Definition) string {
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
if def.Title != "" {
return def.Title
}
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
return split[0]
}
func enumDescriptions(def Definition) (output []string) {
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
if def.Title != "" {
return split
}
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
// so ignore for individual enum descriptions.
return split[2:]
}
type Definition struct {
Properties map[string]struct {
Type string
Ref string `json:"$ref"` // used with object
Items struct { // used with type "array"
Type string
Ref string `json:"$ref"`
}
AdditionalProperties struct {
Type string // used with type "map"
}
Format string // used with type "boolean"
Description string
}
Enum []string
Description string
// used only by enums
Title string
}
func godotClassUtils(p_name string) string {
if val, ok := utilities[p_name]; ok {
return val
}
return ""
}
func main() {
// Argument flags
var output = flag.String("output", "", "The output for generated code.")
flag.Parse()
inputs := flag.Args()
if len(inputs) < 1 {
fmt.Printf("No input file found: %s\n\n", inputs)
fmt.Println("openapi-gen [flags] inputs...")
flag.PrintDefaults()
return
}
input := inputs[0]
content, err := ioutil.ReadFile(input)
if err != nil {
fmt.Printf("Unable to read file: %s\n", err)
return
}
var schema struct {
Paths map[string]map[string]struct {
Summary string
OperationId string
Responses struct {
Ok struct {
Schema struct {
Ref string `json:"$ref"`
}
} `json:"200"`
}
Parameters []struct {
Name string
In string
Required bool
Type string // used with primitives
Items struct { // used with type "array"
Type string
}
Schema struct { // used with http body
Type string
Ref string `json:"$ref"`
}
Format string // used with type "boolean"
}
Security []map[string][]struct {
}
}
Definitions map[string]Definition
}
if err := json.Unmarshal(content, &schema); err != nil {
fmt.Printf("Unable to decode input %s : %s\n", input, err)
return
}
fmap := template.FuncMap{
"cleanRef": convertRefToClassName,
"stripNewlines": stripNewlines,
"title": strings.Title,
"uppercase": strings.ToUpper,
"prependParameter": prependParameter,
"pascalToSnake": pascalToSnake,
"apiFuncName": apiFuncName,
"godotType": godotType,
"godotLooseType": godotLooseType,
"godotSchemaType": godotSchemaType,
"godotDef": godotDef,
"isRefToEnum": func(ref string) bool {
if len(ref) == 0 {
return false
}
// swagger schema definition keys have inconsistent casing
var camelOk bool
var pascalOk bool
var enums []string
asCamel := pascalToCamel(ref)
if _, camelOk = schema.Definitions[asCamel]; camelOk {
enums = schema.Definitions[asCamel].Enum
}
asPascal := camelToPascal(ref)
if _, pascalOk = schema.Definitions[asPascal]; pascalOk {
enums = schema.Definitions[asPascal].Enum
}
if !pascalOk && !camelOk {
fmt.Printf("no definition found: %v", ref)
return false
}
return len(enums) > 0
},
"enumDescriptions": enumDescriptions,
"enumSummary": enumSummary,
"godotClassUtils": godotClassUtils,
}
tmpl, err := template.New(input).Funcs(fmap).Parse(codeTemplate)
if err != nil {
fmt.Printf("Template parse error: %s\n", err)
return
}
if len(*output) < 1 {
tmpl.Execute(os.Stdout, schema)
return
}
f, err := os.Create(*output)
if err != nil {
fmt.Printf("Unable to create file: %s\n", err)
return
}
defer f.Close()
writer := bufio.NewWriter(f)
tmpl.Execute(writer, schema)
writer.Flush()
}