gdscript.core.ApiBee.handlebars Maven / Gradle / Ivy
The newest version!
extends {{>partials/api_base_parent_class}}
class_name {{>partials/api_base_class_name}}
{{>partials/disclaimer_autogenerated}}
# Base class for all generated API endpoints
# ==========================================
#
# Every property/method defined here may collide with userland,
# so these are all listed and excluded in our CodeGen Java file.
# We want to keep the amount of renaming to a minimum, though.
# Therefore, we use the _bzz_ prefix, even if awkward.
const BZZ_CONTENT_TYPE_TEXT := "text/plain"
const BZZ_CONTENT_TYPE_HTML := "text/html"
const BZZ_CONTENT_TYPE_JSON := "application/json"
const BZZ_CONTENT_TYPE_FORM := "application/x-www-form-urlencoded"
const BZZ_CONTENT_TYPE_JSONLD := "application/json+ld" # unsupported (for now)
const BZZ_CONTENT_TYPE_XML := "application/xml" # unsupported (for now)
# From this client's point of view.
# Adding a content type here won't magically make the client support it, but you may reorder.
# These are sorted by decreasing preference. (first → preferred)
const BZZ_PRODUCIBLE_CONTENT_TYPES := [
BZZ_CONTENT_TYPE_JSON,
BZZ_CONTENT_TYPE_FORM,
]
# From this client's point of view.
# Adding a content type here won't magically make the client support it, but you may reorder.
# These are sorted by decreasing preference. (first → preferred)
const BZZ_CONSUMABLE_CONTENT_TYPES := [
BZZ_CONTENT_TYPE_JSON,
]
# Godot's HTTP Client this Api instance is using.
# If none was set (by you), we'll lazily make one.
var _bzz_client: HTTPClient:
set(value):
_bzz_client = value
get:
if not _bzz_client:
_bzz_client = HTTPClient.new()
return _bzz_client
# General configuration that can be shared across Api instances for convenience.
# If no configuration was provided, we'll lazily make one with defaults,
# but you probably want to make your own with your own domain and scheme.
var _bzz_config: {{>partials/api_config_class_name}}:
set(value):
_bzz_config = value
get:
if not _bzz_config:
_bzz_config = {{>partials/api_config_class_name}}.new()
return _bzz_config
# Useful in logs
var _bzz_name: String:
get:
return _bzz_get_api_name()
# Constructor, where you probably want to inject your configuration,
# and as Godot recommends re-using HTTP clients, your client as well.
func _init(config : {{>partials/api_config_class_name}} = null, client : HTTPClient = null):
if config != null:
self._bzz_config = config
if client != null:
self._bzz_client = client
{{! We'll probably only use this for logging. }}
{{! Each Api child can define its own, and it should be similar to class_name. }}
{{! https://github.com/godotengine/godot/issues/21789 }}
func _bzz_get_api_name() -> String:
return "ApiBee"
func _bzz_next_loop_iteration():
# I can't find `idle_frame` in 4.0, but we probably want idle_frame here
return Engine.get_main_loop().process_frame
func _bzz_connect_client_if_needed(
on_success: Callable, # func()
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
#finally: Callable,
):
if (
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTED
or
self._bzz_client.get_status() == HTTPClient.STATUS_RESOLVING
or
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_REQUESTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_BODY
):
on_success.call()
var connecting := self._bzz_client.connect_to_host(
self._bzz_config.host, self._bzz_config.port, self._bzz_config.tls_options
)
if connecting != OK:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = connecting
error.identifier = "apibee.connect_to_host.failure"
error.message = "%s: failed to connect to `%s' port `%d' with error: %s" % [
_bzz_name, self._bzz_config.host, self._bzz_config.port,
_bzz_httpclient_status_string(connecting),
]
on_failure.call(error)
return
# Wait until resolved and connected.
while (
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_RESOLVING
):
self._bzz_client.poll()
self._bzz_config.log_debug("Connecting…")
if self._bzz_config.polling_interval_ms:
OS.delay_msec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
var connected := self._bzz_client.get_status()
if connected != HTTPClient.STATUS_CONNECTED:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = connected as Error
error.identifier = "apibee.connect_to_host.status_failure"
error.message = "%s: failed to connect to `%s' port `%d' : %s" % [
_bzz_name, self._bzz_config.host, self._bzz_config.port,
_bzz_httpclient_status_string(connected),
]
on_failure.call(error)
return
on_success.call()
func _bzz_request(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized and sent
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
# This method does not handle full deserialization, it only handles decode and not denormalization.
# Denormalization is handled in each generated API endpoint in the on_success callable of this method.
_bzz_request_text(
method, path, headers, query, body,
func(response):
var mime: String = response.headers['Mime']
var decodedBody # Variant
# Isn't there a match/case now in Gdscript?
if BZZ_CONTENT_TYPE_TEXT == mime:
decodedBody = response.body
elif BZZ_CONTENT_TYPE_HTML == mime:
decodedBody = response.body
elif BZZ_CONTENT_TYPE_JSON == mime:
var parser := JSON.new()
var parsing := parser.parse(response.body)
if OK != parsing:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = parsing
error.identifier = "apibee.decode.cannot_parse_json"
error.response_code = response.code
error.response = response
error.message = "%s: failed to parse JSON response at line %d.\n%s" % [
_bzz_name, parser.get_error_line(), parser.get_error_message()
]
on_failure.call(error)
return
decodedBody = parser.data
else:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = ERR_INVALID_DATA
error.identifier = "apibee.decode.mime_type_unsupported"
error.response_code = response.code
error.response = response
error.message = "%s: mime type `%s' is not supported (yet -- MRs welcome)" % [
_bzz_name, mime
]
on_failure.call(error)
return
response.data = decodedBody
on_success.call(response)
,
func(error):
on_failure.call(error)
,
)
func _bzz_request_text(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
_bzz_connect_client_if_needed(
func():
_bzz_do_request_text(method, path, headers, query, body, on_success, on_failure)
,
func(error):
on_failure.call(error)
,
)
func _bzz_do_request_text(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
headers = headers.duplicate(true)
headers.merge(self._bzz_config.headers_base)
headers.merge(self._bzz_config.headers_override, true)
var body_normalized = body
if body is Object:
if body.has_method('bzz_collect_missing_properties'):
var missing_properties : Array = body.bzz_collect_missing_properties()
if missing_properties:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.request.body.missing_properties"
error.message = "%s: `%s' is missing required properties %s." % [
_bzz_name, body.bzz_class_name, missing_properties
]
on_failure.call(error)
return
if body.has_method('bzz_normalize'):
body_normalized = body.bzz_normalize()
var body_serialized := ""
var content_type := self._bzz_get_content_type(headers)
if content_type == BZZ_CONTENT_TYPE_JSON:
body_serialized = JSON.stringify(body_normalized)
elif content_type == BZZ_CONTENT_TYPE_FORM:
body_serialized = self._bzz_client.query_string_from_dict(body_normalized)
else:
# TODO: Handle other serialization schemes (json+ld, xml…)
push_warning("Unsupported content-type `%s`." % content_type)
var path_queried := path
var query_string := self._bzz_client.query_string_from_dict(query)
if query_string:
path_queried = "%s?%s" % [path, query_string]
{{! Godot HTTP Client expects an array of strings, not a dictionary }}
var headers_for_godot := Array() # of String
for key in headers:
headers_for_godot.append("%s: %s" % [key, headers[key]])
self._bzz_config.log_info("%s: REQUEST %s %s" % [_bzz_name, method, path_queried])
if not headers.is_empty():
self._bzz_config.log_debug("→ HEADERS: %s" % [str(headers)])
if body_serialized:
self._bzz_config.log_debug("→ BODY: \n%s" % [body_serialized])
var requesting := self._bzz_client.request(method, path_queried, headers_for_godot, body_serialized)
if requesting != OK:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = requesting
error.identifier = "apibee.request.failure"
error.message = "%s: failed to request to path `%s'." % [
_bzz_name, path
]
on_failure.call(error)
return
# Keep polling for as long as the request is being processed.
while self._bzz_client.get_status() == HTTPClient.STATUS_REQUESTING:
self._bzz_config.log_debug("Requesting…")
if self._bzz_config.polling_interval_ms:
OS.delay_msec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
self._bzz_client.poll()
if not self._bzz_client.has_response():
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.request.no_response"
error.message = "%s: request to `%s' returned no response whatsoever. (status=%d)" % [
_bzz_name, path, self._bzz_client.get_status(),
]
on_failure.call(error)
return
var response := {{>partials/api_response_class_name}}.new()
#response.collect_meta_from_client(self._bzz_client) # to refactor
response.code = self._bzz_client.get_response_code()
response.headers = self._bzz_client.get_response_headers_as_dictionary()
# FIXME: extract from headers "Content-Type": "application/json; charset=utf-8"
# Perhaps use a method of {{>partials/api_response_class_name}} for this?
var encoding := "utf-8"
var mime := "application/json"
response.headers['Encoding'] = encoding
response.headers['Mime'] = mime
#response.collect_body_from_client(self._bzz_client, self._bzz_config) # to refactor
# TODO: cap the size of this, perhaps?
var response_bytes := PackedByteArray()
while self._bzz_client.get_status() == HTTPClient.STATUS_BODY:
var chunk = self._bzz_client.read_response_body_chunk()
if chunk.size() == 0: # Got nothing, wait for buffers to fill a bit.
if self._bzz_config.polling_interval_ms:
OS.delay_usec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
else: # Yummy data has arrived
response_bytes = response_bytes + chunk
self._bzz_client.poll()
self._bzz_config.log_info("%s: RESPONSE %d (%d bytes)" % [
_bzz_name, response.code, response_bytes.size()
])
if not response.headers.is_empty():
self._bzz_config.log_debug("→ HEADERS: %s" % str(response.headers))
var response_text: String
if encoding == "utf-8":
response_text = response_bytes.get_string_from_utf8()
elif encoding == "utf-16":
response_text = response_bytes.get_string_from_utf16()
elif encoding == "utf-32":
response_text = response_bytes.get_string_from_utf32()
else:
response_text = response_bytes.get_string_from_ascii()
if response_text:
self._bzz_config.log_debug("→ BODY: \n%s" % response_text)
response.body = response_text
if response.code >= 500:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = ERR_PRINTER_ON_FIRE
error.response_code = response.code
error.response = response
error.identifier = "apibee.response.5xx"
error.message = "%s: request to `%s' made the server hiccup with a %d." % [
_bzz_name, path, response.code
]
error.message += "\n%s" % [
_bzz_format_error_response(response_text)
]
on_failure.call(error)
return
elif response.code >= 400:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.response.4xx"
error.response_code = response.code
error.response = response
error.message = "%s: request to `%s' was denied with a %d." % [
_bzz_name, path, response.code
]
error.message += "\n%s" % [
_bzz_format_error_response(response_text)
]
on_failure.call(error)
return
elif response.code >= 300:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.response.3xx"
error.response_code = response.code
error.response = response
error.message = "%s: request to `%s' was redirected with a %d. We do not support redirects in that client yet." % [
_bzz_name, path, response.code
]
on_failure.call(error)
return
# Should we close ?
#self._bzz_client.close()
on_success.call(response)
func _bzz_convert_http_method(method: String) -> int:
match method:
'GET': return HTTPClient.METHOD_GET
'POST': return HTTPClient.METHOD_POST
'PUT': return HTTPClient.METHOD_PUT
'PATCH': return HTTPClient.METHOD_PATCH
'DELETE': return HTTPClient.METHOD_DELETE
'CONNECT': return HTTPClient.METHOD_CONNECT
'HEAD': return HTTPClient.METHOD_HEAD
'MAX': return HTTPClient.METHOD_MAX
'OPTIONS': return HTTPClient.METHOD_OPTIONS
'TRACE': return HTTPClient.METHOD_TRACE
_:
push_error("%s: unknown http method `%s`, assuming GET." % [
_bzz_name, method
])
return HTTPClient.METHOD_GET
func _bzz_urlize_path_param(anything) -> String:
var serialized := _bzz_escape_path_param(str(anything))
return serialized
func _bzz_escape_path_param(value: String) -> String:
# TODO: escape for URL
return value
func _bzz_get_content_type(headers: Dictionary) -> String:
if headers.has("Content-Type"):
return headers["Content-Type"]
return BZZ_PRODUCIBLE_CONTENT_TYPES[0]
func _bzz_format_error_response(response: String) -> String:
# TODO: handle other (de)serialization schemes
var parser := JSON.new()
var parsing := parser.parse(response)
if OK != parsing:
return response
if not (parser.data is Dictionary):
return response
var s := "ERROR"
if parser.data.has("code"):
s += " %d" % parser.data['code']
if parser.data.has("message"):
s += "\n%s" % parser.data['message']
else:
return response
return s
func _bzz_httpclient_status_info(status: int) -> Dictionary:
# At some point Godot ought to natively implement this and we won't need this "shim" anymore.
match status:
HTTPClient.STATUS_DISCONNECTED: return {
"name": "STATUS_DISCONNECTED",
"description": "Disconnected from the server."
}
HTTPClient.STATUS_RESOLVING: return {
"name": "STATUS_RESOLVING",
"description": "Currently resolving the hostname for the given URL into an IP."
}
HTTPClient.STATUS_CANT_RESOLVE: return {
"name": "STATUS_CANT_RESOLVE",
"description": "DNS failure: Can't resolve the hostname for the given URL."
}
HTTPClient.STATUS_CONNECTING: return {
"name": "STATUS_CONNECTING",
"description": "Currently connecting to server."
}
HTTPClient.STATUS_CANT_CONNECT: return {
"name": "STATUS_CANT_CONNECT",
"description": "Can't connect to the server."
}
HTTPClient.STATUS_CONNECTED: return {
"name": "STATUS_CONNECTED",
"description": "Connection established."
}
HTTPClient.STATUS_REQUESTING: return {
"name": "STATUS_REQUESTING",
"description": "Currently sending request."
}
HTTPClient.STATUS_BODY: return {
"name": "STATUS_BODY",
"description": "HTTP body received."
}
HTTPClient.STATUS_CONNECTION_ERROR: return {
"name": "STATUS_CONNECTION_ERROR",
"description": "Error in HTTP connection."
}
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: return {
"name": "STATUS_TLS_HANDSHAKE_ERROR",
"description": "Error in TLS handshake."
}
return {
"name": "UNKNOWN (%d)" % status,
"description": "Unknown HTTPClient status."
}
func _bzz_httpclient_status_string(status: int) -> String:
var info := _bzz_httpclient_status_info(status)
return "%s (%s)" % [info["description"], info["name"]]
© 2015 - 2024 Weber Informatics LLC | Privacy Policy