639 lines
18 KiB
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()
|
|
}
|