Initial Commit
Initial commit of Code Base.
This commit is contained in:
parent
293b1213e1
commit
c11a4ebbc2
653 changed files with 36893 additions and 1 deletions
606
addons/twitcher/editor/api_generator/twitch_api_generator.gd
Normal file
606
addons/twitcher/editor/api_generator/twitch_api_generator.gd
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIGenerator
|
||||
|
||||
const suffixes: Array[String] = ["Response", "Body", "Opt"]
|
||||
|
||||
const api_output_path = "res://addons/twitcher/generated/"
|
||||
const twitch_api_header : String = """@tool
|
||||
extends Twitcher
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## Interaction with the Twitch REST API.
|
||||
class_name TwitchAPI
|
||||
|
||||
static var _log: TwitchLogger = TwitchLogger.new("TwitchAPI")
|
||||
|
||||
static var instance: TwitchAPI
|
||||
|
||||
## Maximal tries to reauthrorize before giving up the request.
|
||||
const MAX_AUTH_ERRORS = 3
|
||||
|
||||
## Called when the API returns unauthenticated mostly cause the accesstoken is expired
|
||||
signal unauthenticated
|
||||
|
||||
## Called when the API returns 403 means there are permissions / scopes missing
|
||||
signal unauthorized
|
||||
|
||||
## To authorize against the Twitch API
|
||||
@export var token: OAuthToken:
|
||||
set(val):
|
||||
token = val
|
||||
update_configuration_warnings()
|
||||
## OAuth settings needed for client information
|
||||
@export var oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
oauth_setting = val
|
||||
update_configuration_warnings()
|
||||
## URI to the Twitch API
|
||||
@export var api_host: String = "https://api.twitch.tv/helix"
|
||||
|
||||
## Client to make HTTP requests
|
||||
var client: BufferedHTTPClient
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client = BufferedHTTPClient.new()
|
||||
client.name = "ApiClient"
|
||||
add_child(client)
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if instance == null: instance = self
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if instance == self: instance = null
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
if token == null:
|
||||
result.append("Please set a token to use")
|
||||
if oauth_setting == null:
|
||||
result.append("Please set the correct oauth settings")
|
||||
return result
|
||||
|
||||
|
||||
func request(path: String, method: int, body: Variant = "", content_type: String = "", error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
var header : Dictionary = {
|
||||
"Authorization": "Bearer %s" % [await token.get_access_token()],
|
||||
"Client-ID": oauth_setting.client_id
|
||||
}
|
||||
if content_type != "":
|
||||
header["Content-Type"] = content_type
|
||||
|
||||
var request_body: String = ""
|
||||
if body == null || (body is String && body == ""):
|
||||
request_body = ""
|
||||
elif body is Object && body.has_method("to_json"):
|
||||
request_body = body.to_json()
|
||||
else:
|
||||
request_body = JSON.stringify(body)
|
||||
|
||||
var req: BufferedHTTPClient.RequestData = client.request(api_host + path, method, header, request_body)
|
||||
var res: BufferedHTTPClient.ResponseData = await client.wait_for_request(req)
|
||||
|
||||
# Try to fix Godot TLS Bug
|
||||
if res.result == 5:
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
|
||||
match res.response_code:
|
||||
400:
|
||||
var error_message: String = res.response_data.get_string_from_utf8()
|
||||
_log.e("'%s' failed cause of: \\n%s" % [path, error_message])
|
||||
401: # Token expired / or missing permissions
|
||||
_log.e("'%s' is unauthorized. It is probably your scopes." % path)
|
||||
unauthorized.emit()
|
||||
403:
|
||||
_log.i("'%s' is unauthenticated. Refresh token." % path)
|
||||
unauthenticated.emit()
|
||||
await token.authorized
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
return res
|
||||
|
||||
|
||||
func retry(request: BufferedHTTPClient.RequestData,
|
||||
response: BufferedHTTPClient.ResponseData,
|
||||
path: String,
|
||||
method: int,
|
||||
body: Variant = "",
|
||||
content_type: String = "",
|
||||
error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
if error_count + 1 < MAX_AUTH_ERRORS:
|
||||
return await request(path, method, body, content_type, error_count + 1)
|
||||
else:
|
||||
# Give up the request after trying multiple times and
|
||||
# return an empty response with correct error code
|
||||
var empty_response: BufferedHTTPClient.ResponseData = client.empty_response(request)
|
||||
empty_response.response_code = response.response_code
|
||||
return empty_response
|
||||
|
||||
|
||||
## Converts unix timestamp to RFC 3339 (example: 2021-10-27T00:00:00Z) when passed a string uses as is
|
||||
static func get_rfc_3339_date_format(time: Variant) -> String:
|
||||
if typeof(time) == TYPE_INT:
|
||||
var date_time = Time.get_datetime_dict_from_unix_time(time)
|
||||
return "%s-%02d-%02dT%02d:%02d:%02dZ" % [date_time['year'], date_time['month'], date_time['day'], date_time['hour'], date_time['minute'], date_time['second']]
|
||||
return str(time)
|
||||
|
||||
"""
|
||||
|
||||
@export var parser: TwitchAPIParser
|
||||
|
||||
var grouped_files: Dictionary[String, Variant] = {}
|
||||
|
||||
|
||||
func prepare_component(component: TwitchGenComponent) -> void:
|
||||
if component._is_root:
|
||||
var base_name = get_base_name(component._classname)
|
||||
|
||||
# No suffix class lives by its own
|
||||
if base_name == component._classname:
|
||||
if grouped_files.has(base_name):
|
||||
push_error("That file shouldn't exist: %s" % base_name)
|
||||
component._classname = "Twitch" + component._classname
|
||||
grouped_files[base_name] = component
|
||||
else:
|
||||
var file: GroupedComponent = grouped_files.get(base_name, GroupedComponent.new())
|
||||
file.base_name = "Twitch" + base_name
|
||||
file.components.append(component)
|
||||
grouped_files[base_name] = file
|
||||
component._classname = component._classname.trim_prefix(base_name)
|
||||
component.set_meta("fqdn", file.base_name + "." + component._classname)
|
||||
var sub_components_to_update: Array[TwitchGenComponent] = component._sub_components.values().duplicate()
|
||||
for sub_component in sub_components_to_update:
|
||||
sub_component._classname = component._classname + sub_component._classname
|
||||
sub_components_to_update.append_array(sub_component._sub_components.values())
|
||||
pass
|
||||
|
||||
|
||||
|
||||
func generate_api() -> void:
|
||||
for component: Variant in parser.components:
|
||||
prepare_component(component)
|
||||
|
||||
# Generate TwitchAPI
|
||||
var twitch_api_code = twitch_api_header
|
||||
for method: TwitchGenMethod in parser.methods:
|
||||
twitch_api_code += method_code(method)
|
||||
write_output_file(api_output_path + "twitch_api.gd", twitch_api_code)
|
||||
|
||||
# Generate Components
|
||||
for component: Variant in grouped_files.values():
|
||||
var code = ""
|
||||
if component is GroupedComponent:
|
||||
code = group_code(component)
|
||||
else:
|
||||
code = component_code(component, 0)
|
||||
write_output_file(api_output_path + component.get_filename(), code)
|
||||
|
||||
print("API regenerated you can find it under: %s" % api_output_path)
|
||||
|
||||
|
||||
class GroupedComponent extends RefCounted:
|
||||
var base_name: String
|
||||
var prefix: String
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
|
||||
|
||||
func _update_base_name(val: String) -> void:
|
||||
base_name = val
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return base_name.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func group_code(group: GroupedComponent) -> String:
|
||||
var code = """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
class_name {name}
|
||||
""".format({"name": group.base_name})
|
||||
for component in group.components:
|
||||
component._is_root = false
|
||||
code += "\n\n"
|
||||
code += component_code(component, 1)
|
||||
return code
|
||||
|
||||
#region Field Code Generation
|
||||
|
||||
func field_declaration(field: TwitchGenField) -> String:
|
||||
var type = get_type(field._type, field._is_array)
|
||||
return """
|
||||
## {description}
|
||||
@export var {name}: {type}:
|
||||
set(val):
|
||||
{name} = val
|
||||
track_data(&"{name}", val)\n""".format({
|
||||
"name": field._name,
|
||||
"description": ident(field._description, 0, "## "),
|
||||
"type": type
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Code Generation
|
||||
|
||||
#func get_code() -> String:
|
||||
#if _name == "broadcaster_id":
|
||||
#var default_value = "default_broadcaster_login" if _type == "String" else "[default_broadcaster_login]"
|
||||
#return "%s: %s = %s" % [_name, get_type(), default_value]
|
||||
#return "%s: %s" % [_name, get_type()]
|
||||
|
||||
#endregion
|
||||
|
||||
#region Method Code Generation
|
||||
|
||||
func parameter_doc(method: TwitchGenMethod) -> String:
|
||||
if method._required_parameters.is_empty():
|
||||
return "## [no required query parameters to describe]"
|
||||
var doc : String = ""
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
doc += "## {name} - {documentation} \n".format({
|
||||
'name': parameter._name,
|
||||
'documentation': ident(parameter._description, 0, "## ")
|
||||
})
|
||||
return doc.rstrip("\n")
|
||||
|
||||
|
||||
func parameter_array(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> Array[String]:
|
||||
var parameters : Array[String] = []
|
||||
if method._contains_body: parameters.append(get_parameter("body", method._body_type, false, with_type, fully_qualified))
|
||||
if method._contains_optional: parameters.append(get_parameter("opt", method.get_optional_type(), false, with_type, fully_qualified))
|
||||
|
||||
method._parameters.sort_custom(TwitchGenParameter.sort)
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
parameters.append(get_parameter(parameter._name, parameter._type, parameter._is_array, with_type, fully_qualified))
|
||||
return parameters
|
||||
|
||||
|
||||
func method_parameter(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> String:
|
||||
return ", ".join(parameter_array(method, with_type, fully_qualified))
|
||||
|
||||
|
||||
func path_code(method: TwitchGenMethod) -> String:
|
||||
var body_code : String = "var path = \"%s?\"\n" % method._path
|
||||
|
||||
if method._contains_optional:
|
||||
body_code += "var optionals: Dictionary[StringName, Variant] = {}\n"
|
||||
body_code += "if opt != null: optionals = opt.to_dict()\n"
|
||||
|
||||
for parameter: TwitchGenParameter in method._parameters:
|
||||
if parameter._required:
|
||||
body_code += parameter_path_code(parameter) + "\n"
|
||||
else:
|
||||
body_code += "if optionals.has(\"%s\"):\n" % parameter._name
|
||||
body_code += "\t%s\n" % ident(parameter_path_code(parameter, "optionals."), 1)
|
||||
return body_code
|
||||
|
||||
|
||||
func parameter_path_code(parameter: TwitchGenParameter, prefix: String = "") -> String:
|
||||
var body: String
|
||||
if parameter._is_time:
|
||||
body = "path += \"{key}=\" + get_rfc_3339_date_format({value}) + \"&\""
|
||||
|
||||
elif parameter._is_array:
|
||||
body = """
|
||||
for param in {value}:
|
||||
path += "{key}=" + str(param) + "&" """.trim_prefix("\n\t")
|
||||
else:
|
||||
body = "path += \"{key}=\" + str({value}) + \"&\""
|
||||
|
||||
return body.format({
|
||||
'value': prefix + parameter._name,
|
||||
'key': parameter._name
|
||||
})
|
||||
|
||||
## Exceptional method cause twitch api is not uniform
|
||||
func paging_code_stream_schedule() -> String:
|
||||
return """
|
||||
if parsed_result.data.pagination != null:
|
||||
opt.after = parsed_result.data.pagination.cursor
|
||||
parsed_result.data._next_page = get_channel_stream_schedule.bind(opt, broadcaster_id)\n"""
|
||||
|
||||
|
||||
func paging_code(method: TwitchGenMethod) -> String:
|
||||
if method._name == "get_channel_stream_schedule":
|
||||
return paging_code_stream_schedule()
|
||||
|
||||
var code: String = ""
|
||||
code += "if parsed_result.pagination != null:\n"
|
||||
var after_parameter: TwitchGenParameter = method.get_parameter_by_name("after")
|
||||
var result_component: TwitchGenComponent = get_component(method._result_type)
|
||||
var pagination_parameter: TwitchGenField = result_component.get_field_by_name("pagination")
|
||||
if pagination_parameter == null:
|
||||
print("Check %s paging without paging?" % method._name)
|
||||
pass
|
||||
elif pagination_parameter._type == "String":
|
||||
code += "\tvar cursor: String = parsed_result.pagination\n"
|
||||
else:
|
||||
code += "\tvar cursor: String = parsed_result.pagination.cursor\n"
|
||||
if after_parameter._required:
|
||||
code += "\t{parameter} = cursor\n"
|
||||
else:
|
||||
code += "\topt.{parameter} = cursor\n"
|
||||
code += "\tparsed_result._next_page = {name}.bind({parameters})\n"
|
||||
|
||||
return code.format({
|
||||
"parameter": after_parameter._name,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method)
|
||||
})
|
||||
|
||||
|
||||
func response_code(method: TwitchGenMethod) -> String:
|
||||
var code: String = ""
|
||||
var result_type = get_type(method._result_type, false, true)
|
||||
if result_type != "BufferedHTTPClient.ResponseData":
|
||||
code = """
|
||||
var result: Variant = JSON.parse_string(response.response_data.get_string_from_utf8())
|
||||
var parsed_result: {result_type} = {result_type}.from_json(result)
|
||||
parsed_result.response = response
|
||||
""".format({ 'result_type': result_type })
|
||||
if method._has_paging: code += paging_code(method)
|
||||
code += "return parsed_result"
|
||||
else:
|
||||
code = "return response"
|
||||
return code
|
||||
|
||||
|
||||
func method_code(method: TwitchGenMethod) -> String:
|
||||
return """
|
||||
|
||||
## {summary}
|
||||
##
|
||||
{parameter_doc}
|
||||
##
|
||||
## {doc_url}
|
||||
func {name}({parameters}) -> {result_type}:
|
||||
{path_code}
|
||||
var response: BufferedHTTPClient.ResponseData = await request(path, HTTPClient.METHOD_{method}, {body_variable}, "{content_type}")
|
||||
{response_code}
|
||||
""".format({
|
||||
"summary": method._summary,
|
||||
"parameter_doc": parameter_doc(method),
|
||||
"doc_url": method._doc_url,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method, true, true),
|
||||
"result_type": get_type(method._result_type, false, true),
|
||||
"path_code": ident(path_code(method), 1),
|
||||
"content_type": get_type(method._content_type, false, true),
|
||||
"method": method._http_verb.to_upper(),
|
||||
"body_variable": "body" if method._contains_body else "\"\"",
|
||||
"response_code": ident(response_code(method), 1),
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Code Generation
|
||||
|
||||
|
||||
func component_code(component: TwitchGenComponent, level: int = 0) -> String:
|
||||
var code: String = ""
|
||||
if component._is_root:
|
||||
code += """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## {description}
|
||||
## {ref}
|
||||
class_name {classname}
|
||||
"""
|
||||
else:
|
||||
code = """
|
||||
## {description}
|
||||
## {ref}
|
||||
class {classname} extends TwitchData:
|
||||
"""
|
||||
var class_code : String = ""
|
||||
for field: TwitchGenField in component._fields:
|
||||
class_code += field_declaration(field)
|
||||
|
||||
if component._is_response:
|
||||
class_code += "var response: BufferedHTTPClient.ResponseData"
|
||||
class_code += "\n\n"
|
||||
class_code += create_code(component) + "\n\n"
|
||||
class_code += from_json_code(component)
|
||||
|
||||
if component._has_paging:
|
||||
class_code += "\n\n" + iter_code(component)
|
||||
|
||||
var sub_component_code: String
|
||||
for sub_component in component._sub_components.values():
|
||||
sub_component_code += "\n\n" + component_code(sub_component, 1)
|
||||
|
||||
return code.format({
|
||||
"description": ident(component._description, 0, "## "),
|
||||
"classname": component._classname,
|
||||
"ref": component._ref
|
||||
}) + ident(class_code, level) + sub_component_code
|
||||
|
||||
|
||||
func create_code(component: TwitchGenComponent) -> String:
|
||||
var parameters: Array[String] = []
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
parameters.append("_" + get_parameter(field._name, field._type, field._is_array))
|
||||
|
||||
var variable_name = component._classname.to_snake_case()
|
||||
var code : String = """
|
||||
## Constructor with all required fields.
|
||||
static func create({parameters}) -> {classname}:
|
||||
var {variablename}: {classname} = {classname}.new()\n""".format({
|
||||
"parameters": ", ".join(parameters),
|
||||
"classname": component._classname,
|
||||
"variablename": variable_name
|
||||
})
|
||||
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
code += "\t{classname}.{name} = _{name}\n".format({
|
||||
"name": field._name,
|
||||
"classname": variable_name
|
||||
})
|
||||
|
||||
code += "\treturn %s" % variable_name
|
||||
return code
|
||||
|
||||
|
||||
func from_json_code(component: TwitchGenComponent) -> String:
|
||||
var code : String = """
|
||||
static func from_json(d: Dictionary) -> {classname}:
|
||||
var result: {classname} = {classname}.new()
|
||||
""".format({"classname": component._classname})
|
||||
for field: TwitchGenField in component._fields:
|
||||
code += "\tif d.get(\"{name}\", null) != null:\n"
|
||||
if field._is_typed_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append({type}.from_json(value))\n""".lstrip("\n")
|
||||
elif field._is_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append(value)\n""".lstrip("\n")
|
||||
elif field._is_sub_class:
|
||||
code += "\t\tresult.{name} = {type}.from_json(d[\"{name}\"])\n"
|
||||
else:
|
||||
code += "\t\tresult.{name} = d[\"{name}\"]\n"
|
||||
code = code.format({
|
||||
"name": field._name,
|
||||
"type": get_type(field._type, false)
|
||||
})
|
||||
code += "\treturn result\n"
|
||||
|
||||
return code
|
||||
|
||||
|
||||
func iter_code(component: TwitchGenComponent) -> String:
|
||||
var data_variable_name: String = "data"
|
||||
var path_to_data: String = ""
|
||||
if component._ref == "#/components/schemas/GetChannelStreamScheduleResponse/Data":
|
||||
data_variable_name = "segments"
|
||||
path_to_data = "data."
|
||||
|
||||
var code: String
|
||||
if component._ref == "#/components/schemas/GetExtensionLiveChannelsResponse":
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null || pagination == "": return false
|
||||
return true
|
||||
"""
|
||||
else:
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null: return false
|
||||
if pagination.cursor == null || pagination.cursor == "": return false
|
||||
return true
|
||||
"""
|
||||
|
||||
code += """
|
||||
var _next_page: Callable
|
||||
var _cur_iter: int = 0
|
||||
|
||||
|
||||
func next_page() -> {response_type}:
|
||||
var response: {response_type} = await _next_page.call()
|
||||
_cur_iter = 0
|
||||
_next_page = response.{path_to_data}_next_page
|
||||
{copy_code}
|
||||
return response
|
||||
|
||||
|
||||
func _iter_init(iter: Array) -> bool:
|
||||
if {data_variable_name}.is_empty(): return false
|
||||
iter[0] = {data_variable_name}[0]
|
||||
return {data_variable_name}.size() > 0
|
||||
|
||||
|
||||
func _iter_next(iter: Array) -> bool:
|
||||
if {data_variable_name}.size() - 1 > _cur_iter:
|
||||
_cur_iter += 1
|
||||
iter[0] = {data_variable_name}[_cur_iter]
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && not _has_pagination():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _iter_get(iter: Variant) -> Variant:
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && _has_pagination():
|
||||
await next_page()
|
||||
return iter"""
|
||||
var copy_code: String
|
||||
for field in component._fields:
|
||||
copy_code += "\t{_name} = response.{path_to_data}{_name}\n".format(field)
|
||||
|
||||
return code.format({
|
||||
"data_variable_name": data_variable_name,
|
||||
"copy_code": copy_code,
|
||||
"path_to_data": path_to_data,
|
||||
"response_type": component.get_root_classname()
|
||||
})
|
||||
#endregion
|
||||
|
||||
#region Utils
|
||||
|
||||
|
||||
func get_type(type: String, is_array: bool = false, full_qualified: bool = false) -> String:
|
||||
var result_type : String = ""
|
||||
if type.begins_with("#"):
|
||||
var component: TwitchGenComponent = parser.get_component_by_ref(type)
|
||||
result_type = component._classname
|
||||
if full_qualified and component.has_meta("fqdn"):
|
||||
result_type = component.get_meta("fqdn")
|
||||
else:
|
||||
result_type = type
|
||||
return result_type if not is_array else "Array[%s]" % result_type
|
||||
|
||||
|
||||
func get_component(type: String) -> TwitchGenComponent:
|
||||
if type.begins_with("#"):
|
||||
return parser.get_component_by_ref(type)
|
||||
else:
|
||||
return null
|
||||
|
||||
func ident(code: String, level: int, padding: String = "") -> String:
|
||||
return code.replace("\n", "\n" + "\t".repeat(level) + padding)
|
||||
|
||||
|
||||
# Writes the processed content to the output file.
|
||||
func write_output_file(file_output: String, content: String) -> void:
|
||||
var file = FileAccess.open(file_output, FileAccess.WRITE);
|
||||
if file == null:
|
||||
var error_message = error_string(FileAccess.get_open_error());
|
||||
push_error("Failed to open output file: %s\n%s" % [file_output, error_message])
|
||||
return
|
||||
file.store_string(content)
|
||||
file.flush()
|
||||
file.close()
|
||||
|
||||
|
||||
func get_base_name(file: String) -> String:
|
||||
var new_file: String = file
|
||||
for suffix: String in suffixes:
|
||||
new_file = new_file.trim_suffix(suffix)
|
||||
return new_file
|
||||
|
||||
|
||||
func get_parameter(title: String, type: String, is_array = false, with_type: bool = true, fully_qualified: bool = false) -> String:
|
||||
if with_type:
|
||||
return "{name}: {type}".format({
|
||||
"name": title,
|
||||
"type": get_type(type, is_array, fully_qualified)
|
||||
})
|
||||
else:
|
||||
return title
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cetl2un34bjb1
|
||||
247
addons/twitcher/editor/api_generator/twitch_api_parser.gd
Normal file
247
addons/twitcher/editor/api_generator/twitch_api_parser.gd
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIParser
|
||||
|
||||
const SWAGGER_API = "https://raw.githubusercontent.com/DmitryScaletta/twitch-api-swagger/refs/heads/main/openapi.json"
|
||||
|
||||
var definition: Dictionary = {}
|
||||
var component_map: Dictionary[String, TwitchGenComponent] = {}
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
var methods: Array[TwitchGenMethod] = []
|
||||
|
||||
var client: BufferedHTTPClient = BufferedHTTPClient.new()
|
||||
|
||||
signal component_added(component: TwitchGenComponent)
|
||||
signal method_added(method: TwitchGenMethod)
|
||||
|
||||
|
||||
class ComponentRepo extends RefCounted:
|
||||
var _component: TwitchGenComponent
|
||||
var _component_map: Dictionary[String, TwitchGenComponent]
|
||||
|
||||
|
||||
func get_comp(component_name: String) -> TwitchGenComponent:
|
||||
var component = _component.get_component(component_name)
|
||||
if component != null: return component
|
||||
return _component_map.get(component_name)
|
||||
|
||||
|
||||
func _init(component: TwitchGenComponent, component_map: Dictionary[String, TwitchGenComponent]) -> void:
|
||||
_component = component
|
||||
_component_map = component_map
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client.name = "APIGeneratorClient"
|
||||
|
||||
|
||||
func parse_api() -> void:
|
||||
print("start generating API")
|
||||
if definition == {}:
|
||||
print("load Twitch definition")
|
||||
definition = await _load_swagger_definition()
|
||||
|
||||
_parsing_components()
|
||||
_parsing_paths()
|
||||
|
||||
|
||||
func _load_swagger_definition() -> Dictionary:
|
||||
add_child(client)
|
||||
client.max_error_count = 3
|
||||
var request = client.request(SWAGGER_API, HTTPClient.METHOD_GET, {}, "")
|
||||
var response_data = await client.wait_for_request(request)
|
||||
|
||||
if response_data.error:
|
||||
printerr("Cant generate API")
|
||||
return {}
|
||||
var response_str = response_data.response_data.get_string_from_utf8()
|
||||
var response = JSON.parse_string(response_str)
|
||||
remove_child(client)
|
||||
return response
|
||||
|
||||
|
||||
func _parsing_components() -> void:
|
||||
var schemas = definition["components"]["schemas"]
|
||||
for schema_name in schemas:
|
||||
var schema: Dictionary = schemas[schema_name]
|
||||
if schema["type"] != "object":
|
||||
printerr("Not an object")
|
||||
continue
|
||||
|
||||
var ref = "#/components/schemas/" + schema_name
|
||||
var component = TwitchGenComponent.new(schema_name, ref)
|
||||
component._is_root = true
|
||||
component._is_response = true
|
||||
_parse_properties(component, schema)
|
||||
_add_component(ref, component)
|
||||
|
||||
|
||||
func _parse_properties(component: TwitchGenComponent, schema: Dictionary) -> void:
|
||||
var properties = schema["properties"]
|
||||
for property_name: String in properties:
|
||||
var property: Dictionary = properties[property_name]
|
||||
var field: TwitchGenField = TwitchGenField.new()
|
||||
field._name = property_name
|
||||
field._description = property.get("description", "")
|
||||
field._type = _get_param_type(property)
|
||||
|
||||
var classname: String = property_name.capitalize().replace(" ", "")
|
||||
|
||||
|
||||
if property.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, property)
|
||||
field._type = sub_component._ref
|
||||
|
||||
## Arrays that has custom types
|
||||
elif property.get("type", "") == "array":
|
||||
field._is_array = true
|
||||
field._is_sub_class = false
|
||||
var items = property.get("items", {})
|
||||
if items.has("$ref"):
|
||||
field._type = items.get("$ref")
|
||||
elif items.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, items)
|
||||
field._type = sub_component._ref
|
||||
|
||||
component.add_field(field)
|
||||
|
||||
var requires: Array = schema.get("required", [])
|
||||
for required_field: String in requires:
|
||||
var field: TwitchGenField = component.get_field_by_name(required_field)
|
||||
field._is_required = true
|
||||
|
||||
|
||||
func _add_sub_component(classname: String, description: String, parent_component: TwitchGenComponent, properties: Dictionary) -> TwitchGenComponent:
|
||||
var ref: String = parent_component._ref + "/" + classname
|
||||
var sub_component = TwitchGenComponent.new(classname, ref)
|
||||
sub_component._description = description
|
||||
_parse_properties(sub_component, properties)
|
||||
parent_component.add_component(sub_component)
|
||||
_add_component(ref, sub_component)
|
||||
return sub_component
|
||||
|
||||
|
||||
func _parsing_paths() -> void:
|
||||
var paths = definition.get("paths", {})
|
||||
for path in paths:
|
||||
var method_specs = paths[path]
|
||||
for http_verb: String in method_specs:
|
||||
var method_spec = method_specs[http_verb] as Dictionary
|
||||
var method = _parse_method(http_verb, method_spec)
|
||||
method._path = path
|
||||
if method._contains_optional:
|
||||
var component : TwitchGenComponent = method.get_optional_component()
|
||||
_add_component(component._ref, component)
|
||||
methods.append(method)
|
||||
method_added.emit(method)
|
||||
|
||||
|
||||
func _parse_method(http_verb: String, method_spec: Dictionary) -> TwitchGenMethod:
|
||||
var method: TwitchGenMethod = TwitchGenMethod.new()
|
||||
method._http_verb = http_verb
|
||||
method._name = method_spec.get("operationId", "method_" + http_verb).replace("-", "_")
|
||||
method._summary = method_spec.get("summary", "No summary provided.")
|
||||
method._description = method_spec.get("description", "No description provided.")
|
||||
method._doc_url = method_spec.get("externalDocs", {}).get("url", "No link provided")
|
||||
_parse_parameters(method, method_spec)
|
||||
|
||||
# Body Type
|
||||
if method_spec.has("requestBody"):
|
||||
method._body_type = "Dictionary"
|
||||
var ref = method_spec.get("requestBody").get("content", {}).get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._body_type = ref
|
||||
|
||||
# Result Type
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
var responses = method_spec.get("responses", {})
|
||||
if responses.has("200") || responses.has("202"):
|
||||
var content: Dictionary = {}
|
||||
if responses.has("200"):
|
||||
content = responses["200"].get("content", {})
|
||||
elif content == {}:
|
||||
content = responses["202"].get("content", {})
|
||||
|
||||
# Assuming the successful response is a JSON object
|
||||
method._result_type = "Dictionary"
|
||||
|
||||
# Special case for /schedule/icalendar
|
||||
if content.has("text/calendar"):
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
|
||||
|
||||
# Try to resolve the component references
|
||||
var ref = content.get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._result_type = ref
|
||||
|
||||
# Content Type
|
||||
if method_spec.has("requestBody"):
|
||||
var requestBody = method_spec.get("requestBody")
|
||||
var content = requestBody.get("content")
|
||||
method._content_type = content.keys()[0]
|
||||
elif http_verb == "POST":
|
||||
method._content_type = "application/x-www-form-urlencoded"
|
||||
return method
|
||||
|
||||
|
||||
func _parse_parameters(method: TwitchGenMethod, method_spec: Dictionary) -> void:
|
||||
var parameter_specs = method_spec.get("parameters", [])
|
||||
for parameter_spec in parameter_specs:
|
||||
var parameter: TwitchGenParameter = TwitchGenParameter.new()
|
||||
var schema = parameter_spec["schema"]
|
||||
parameter._name = parameter_spec.get("name", "")
|
||||
parameter._description = parameter_spec.get("description", "")
|
||||
parameter._type = _get_param_type(schema)
|
||||
parameter._required = parameter_spec.get("required", false)
|
||||
parameter._is_time = schema.get("format", "") == "date-time"
|
||||
parameter._is_array = schema.get("type", "") == "array"
|
||||
method.add_parameter(parameter)
|
||||
|
||||
|
||||
func _add_component(ref: String, component: TwitchGenComponent) -> void:
|
||||
components.append(component)
|
||||
component_map[ref] = component
|
||||
|
||||
|
||||
func get_component_by_ref(ref: String) -> TwitchGenComponent:
|
||||
return component_map[ref]
|
||||
|
||||
|
||||
func _get_param_type(schema: Dictionary) -> String:
|
||||
if schema.has("$ref"):
|
||||
return schema["$ref"]
|
||||
|
||||
if not schema.has("type"):
|
||||
return "Variant" # Maybe ugly
|
||||
|
||||
var type = schema["type"]
|
||||
var format = schema.get("format", "")
|
||||
match type:
|
||||
"object":
|
||||
if schema.has("additinalProperties"):
|
||||
return _get_param_type(schema["additinalProperties"])
|
||||
return "Dictionary"
|
||||
"string":
|
||||
# Why did I do this in the first place?
|
||||
# Lets disable and see if problems appear
|
||||
#if format == "date-time":
|
||||
# return "Variant"
|
||||
return "String"
|
||||
"integer":
|
||||
return "int"
|
||||
"number":
|
||||
return "float" if format == "float" else "int"
|
||||
"boolean":
|
||||
return "bool"
|
||||
"array":
|
||||
var ref: String = schema["items"].get("$ref", "")
|
||||
if schema["items"].get("type", "") == "string":
|
||||
return "String"
|
||||
elif ref != "":
|
||||
return ref
|
||||
else:
|
||||
return "Variant"
|
||||
_: return "Variant"
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://o7q04krfh33l
|
||||
3
addons/twitcher/editor/api_generator/twitch_gen.gd
Normal file
3
addons/twitcher/editor/api_generator/twitch_gen.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends RefCounted
|
||||
|
||||
class_name TwitchGen
|
||||
1
addons/twitcher/editor/api_generator/twitch_gen.gd.uid
Normal file
1
addons/twitcher/editor/api_generator/twitch_gen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvtstbwwwf7gf
|
||||
60
addons/twitcher/editor/api_generator/twitch_gen_component.gd
Normal file
60
addons/twitcher/editor/api_generator/twitch_gen_component.gd
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenComponent
|
||||
|
||||
var _classname: String
|
||||
var _ref: String
|
||||
var _description: String
|
||||
var _fields: Array[TwitchGenField] = []
|
||||
var _field_map: Dictionary[String, TwitchGenField] = {}
|
||||
var _parent_component: TwitchGenComponent
|
||||
var _sub_components: Dictionary[String, TwitchGenComponent] = {}
|
||||
var _is_root: bool
|
||||
var _is_response: bool
|
||||
var _has_paging: bool
|
||||
var _filename: String:
|
||||
get(): return _classname.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _init(classname: String, ref: String) -> void:
|
||||
_classname = _sanatize_classname(classname)
|
||||
_ref = ref
|
||||
|
||||
|
||||
func add_field(field: TwitchGenField) -> void:
|
||||
_fields.append(field)
|
||||
_field_map[field._name] = field
|
||||
if field._name == "pagination":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_field_by_name(field_name: String) -> TwitchGenField:
|
||||
return _field_map.get(field_name, null)
|
||||
|
||||
|
||||
func get_root_classname() -> String:
|
||||
var parent: TwitchGenComponent = self
|
||||
while parent._parent_component != null:
|
||||
parent = parent._parent_component
|
||||
return parent._classname
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return get_root_classname().to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _sanatize_classname(val: String) -> String:
|
||||
match val:
|
||||
"Image": return "TwitchImage"
|
||||
"Panel": return "TwitchPanel"
|
||||
_: return val
|
||||
|
||||
|
||||
func get_component(component_name: String) -> TwitchGenComponent:
|
||||
return _sub_components.get(component_name)
|
||||
|
||||
|
||||
func add_component(sub_component: TwitchGenComponent) -> void:
|
||||
_sub_components[sub_component._classname] = sub_component
|
||||
sub_component._parent_component = self
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cttaojg743q5y
|
||||
31
addons/twitcher/editor/api_generator/twitch_gen_field.gd
Normal file
31
addons/twitcher/editor/api_generator/twitch_gen_field.gd
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenField
|
||||
|
||||
var _name: String:
|
||||
set = _update_name
|
||||
var _description: String
|
||||
var _type: String
|
||||
var _is_required: bool
|
||||
var _is_sub_class: bool:
|
||||
get(): return _type.begins_with("#")
|
||||
var _is_array: bool
|
||||
var _is_typed_array: bool:
|
||||
get(): return _is_array && _type.begins_with("#")
|
||||
|
||||
|
||||
## Couple of names from the Twitch API are messed up like keywords for godot or numbers
|
||||
func _update_name(val: String) -> void:
|
||||
match val:
|
||||
"animated": _name = "animated_format"
|
||||
"static": _name = "static_format"
|
||||
"1": _name = "_1"
|
||||
"2": _name = "_2"
|
||||
"3": _name = "_3"
|
||||
"4": _name = "_4"
|
||||
"1.5": _name = "_1_5"
|
||||
"100x100": _name = "_100x100"
|
||||
"24x24": _name = "_24x24"
|
||||
"300x200": _name = "_300x200"
|
||||
_: _name = val
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dgx1w74garjdh
|
||||
59
addons/twitcher/editor/api_generator/twitch_gen_method.gd
Normal file
59
addons/twitcher/editor/api_generator/twitch_gen_method.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenMethod
|
||||
var _http_verb: String
|
||||
var _name: String
|
||||
var _summary: String
|
||||
var _description: String
|
||||
var _path: String
|
||||
var _doc_url: String
|
||||
var _parameters: Array[TwitchGenParameter] = []
|
||||
var _parameter_map: Dictionary[String, TwitchGenParameter] = {}
|
||||
var _required_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return p._required)
|
||||
var _optional_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return not p._required)
|
||||
var _body_type: String
|
||||
var _result_type: String
|
||||
var _content_type: String
|
||||
var _has_paging: bool
|
||||
var _contains_optional: bool
|
||||
var _contains_body: bool:
|
||||
get(): return _body_type != null and _body_type != ""
|
||||
|
||||
|
||||
func add_parameter(parameter: TwitchGenParameter) -> void:
|
||||
_parameters.append(parameter)
|
||||
_contains_optional = _contains_optional || not parameter._required
|
||||
_parameter_map[parameter._name] = parameter
|
||||
if parameter._name == "after":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_parameter_by_name(name: String) -> TwitchGenParameter:
|
||||
return _parameter_map.get(name)
|
||||
|
||||
|
||||
func get_optional_classname() -> String:
|
||||
return _name.capitalize().replace(" ", "") + "Opt"
|
||||
|
||||
|
||||
func get_optional_type() -> String:
|
||||
return "#/components/schemas/" + get_optional_classname()
|
||||
|
||||
|
||||
func get_optional_component() -> TwitchGenComponent:
|
||||
var component = TwitchGenComponent.new(get_optional_classname(), get_optional_type())
|
||||
component._description = "All optional parameters for TwitchAPI.%s" % _name
|
||||
component._is_root = true
|
||||
for parameter: TwitchGenParameter in _optional_parameters:
|
||||
var field = TwitchGenField.new()
|
||||
field._name = parameter._name
|
||||
field._type = parameter._type
|
||||
field._description = parameter._description
|
||||
field._is_required = false
|
||||
field._is_array = parameter._is_array
|
||||
component.add_field(field)
|
||||
|
||||
return component
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dkwhf6s838d20
|
||||
22
addons/twitcher/editor/api_generator/twitch_gen_parameter.gd
Normal file
22
addons/twitcher/editor/api_generator/twitch_gen_parameter.gd
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenParameter
|
||||
var _name: String
|
||||
var _description: String
|
||||
var _required: bool
|
||||
var _type: String
|
||||
var _is_time: bool
|
||||
var _is_array: bool
|
||||
|
||||
static func sort(p1: TwitchGenParameter, p2: TwitchGenParameter) -> bool:
|
||||
if p1._name == "broadcaster_id":
|
||||
return false
|
||||
if p2._name == "broadcaster_id":
|
||||
return true
|
||||
if p1._required && not p2._required:
|
||||
return true
|
||||
if not p1._required && p2._required:
|
||||
return false
|
||||
return p1._name < p2._name
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://1ptmbsygcfyt
|
||||
Loading…
Add table
Add a link
Reference in a new issue