[GH-ISSUE #274] Changes needed for the OpenApi [WIP] #192

Open
opened 2026-10-15 12:58:01 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Skerga on GitHub (Oct 15, 2025).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/274

This is currently more of a reminder, but i hope that the following problems with the OpenApi spec can be mended.

Currently I'm trying to get NSwag to generate a working Client for the WolfApi just from the OpenApi.json the Api returns.
In the process I've identified some problems that I'm currently bypass by doing some major Regex replacement.

  • The nullable description is incorrect, possible fix changing the used library function/implementing a custom one that generates valid syntax Here
  • Adding the responses for v1/utils/get-icon and /api/v1/docker/images/inspect
  • I've found no simple way to have runner be both cmd or docker, currently I've removed the cmd option.
  • The Types have hideous and long names, I've removed most of the offender via Regex.
  • Maybe we should define a 204 response for the /api/v1/docker/images/inspect endpoint. that way an empty response would be valid and its not automatically an Exception to not find an Image.

Here the RegexReplace calls I'm currently using (c# syntax "" escapes " in an @ string). I was modifying the spec directly at this point but the building time for wolf was taking too long for me so I've put them in the c# Source Generator.

// replaces all type null with nullable:true, needs more cleanup after
text = Regex.Replace(text, """{"type":"null"}""", """{"nullable":true}""");
// removes anyOf if only one type and nullable:true are inside.
text = Regex.Replace(text, @"""anyOf"":\[{""type"":""([aA-zZ]+)""},{""nullable"":true}]", @"""type"":""$1"",""nullable"":true");
// remove empty required arrays
text = Regex.Replace(text, @",\""required\"":\[]", "");
// change number string to number value
text = Regex.Replace(text, @"""(min|max)Items"":""([0-9]+)""", @"""$1Items"":$2");
// patching in the response content for get-icon
text = Regex.Replace(text, @"(v1\/utils\/get-icon"":{.*?,""responses"":{)}", 
    @"$1""200"":{""content"":{""image/png"":{""schema"":{""type"":""string"",""format"":""binary""}}},""description"":""""}}");
// force runner to be docker runner only, multiple different objects aren't possible  
text = Regex.Replace(text, @"(""runner"":{""anyOf"":\[.*?])}", @"""runner"":{""$ref"":""#/components/schemas/wolf__config__AppDocker__tagged""}");
// removes the enum from type, since only docker is used here.
text = Regex.Replace(text, @"""type"":{""type"":""string"",""enum"":\[.*?]}", @"""type"":{""type"":""string""}");
// Make PartialClientSettings correctly optional
text = Regex.Replace(text, @"""(client_settings|settings)"":{""description"":"".*?"",""anyOf"":\[{""\$ref"":""(.*?)""},{""nullable"":true}]}", @"""$1"":{""nullable"":true,""allOf"":[{""$ref"":""$2""}]}");
// fixes optional Pin fields, since they shouldn't be "anyOf"
text = Regex.Replace(text, @"""anyOf"":\[{""type"":""array"",""items"":{""type"":""integer""}},{""nullable"":true}]", @"""type"":""array"",""items"":{""type"":""integer""},""nullable"":true");
// sanitize reflection type names.
text = Regex.Replace(text, @"rfl__Reflector_wolf__core__events__(.*?)___ReflType", @"$1");
// sanitize endpoint return type names
text = Regex.Replace(text, @"wolf__api__(.*?)""", @"$1""");
// sanitize events type names
text = Regex.Replace(text, @"wolf__core__events__(.*?)""", @"$1""");

I've also written a separate OpenApi file to define the /api/v1/docker/images/inspect endpoint:

{
    "openapi": "3.1.0",
    "info": {
        "title": "Wolf API",
        "description": "API for the Wolf server",
        "version": "0.1"
    },
    "servers": [
        {
            "url": "http://localhost/",
            "description": "Local development server"
        }
    ],
    "paths": {
        "/api/v1/docker/images/inspect": {
            "get": {
                "parameters": [
                    {
                        "in": "query",
                        "name": "image_name",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "summary": "Inspect a Docker image and returns the full JSON response as is from the Docker APIs at /images/{image_name}/json expects image_name as a query parameter.",
                "description": "",
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DockerInspect"
                                }
                            }
                        },
                        "description": ""
                    },
                    "404": {
                        "description": "Image not found"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "DockerInspect": {
                "type": "object",
                "properties": {
                    "Id": {
                        "type": "string"
                    },
                    "RepoTags": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "RepoDigests": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Parent": {
                        "type": "string"
                    },
                    "Comment": {
                        "type": "string"
                    },
                    "Created": {
                        "type": "string"
                    },
                    "ContainerConfig": {
                        "$ref": "#/components/schemas/ContainerConfig"
                    },
                    "DockerVersion": {
                        "type": "string"
                    },
                    "Author": {
                        "type": "string"
                    },
                    "Architecture": {
                        "type": "string"
                    },
                    "Os": {
                        "type": "string"
                    },
                    "Size": {
                        "type": "number",
                        "format": "int64"
                    },
                    "VirtualSize": {
                        "type": "number",
                        "format": "int64"
                    },
                    "GraphDriver": {
                        "$ref": "#/components/schemas/GraphDriver"
                    },
                    "RootFS": {
                        "$ref": "#/components/schemas/RootFS"
                    },
                    "Metadata": {
                        "nullable": true,
                        "type": "object",
                        "additionalProperties": {
                            "type": "string"
                        }
                    },
                    "Config": {
                        "$ref": "#/components/schemas/Config"
                    }
                }
            },
            "ContainerConfig": {
                "type": "object",
                "properties": {
                    "Hostname": {
                        "type": "string"
                    },
                    "Domainname": {
                        "type": "string"
                    },
                    "User": {
                        "type": "string"
                    },
                    "AttachStdin": {
                        "type": "boolean"
                    },
                    "AttachStdout": {
                        "type": "boolean"
                    },
                    "AttachStderr": {
                        "type": "boolean"
                    },
                    "Tty": {
                        "type": "boolean"
                    },
                    "OpenStdin": {
                        "type": "boolean"
                    },
                    "StdinOnce": {
                        "type": "boolean"
                    },
                    "Env": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Cmd": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Image": {
                        "type": "string"
                    },
                    "Volumes": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "WorkingDir": {
                        "type": "string"
                    },
                    "Entrypoint": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "OnBuild": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Labels": {
                        "nullable": true,
                        "type": "object",
                        "additionalProperties": {
                            "type": "string"
                        }
                    }
                },
                "required": [
                    "Hostname",
                    "Domainname",
                    "User",
                    "AttachStdin",
                    "AttachStdout",
                    "AttachStderr",
                    "Tty",
                    "OpenStdin",
                    "StdinOnce",
                    "Image",
                    "WorkDir"
                ]
            },
            "RootFS": {
                "type": "object",
                "properties": {
                    "Type": {
                        "type": "string"
                    },
                    "Layers": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    }
                }
            },
            "Config": {
                "type": "object",
                "properties": {
                    "AttachStderr": {
                        "type": "boolean"
                    },
                    "AttachStdin": {
                        "type": "boolean"
                    },
                    "AttachStdout": {
                        "type": "boolean"
                    },
                    "Cmd": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Domainname": {
                        "type": "string"
                    },
                    "Entrypoint": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Env": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "Hostname": {
                        "type": "string"
                    },
                    "Image": {
                        "type": "string"
                    },
                    "Labels": {
                        "nullable": true,
                        "type": "object",
                        "additionalProperties": {
                            "type": "string"
                        }
                    },
                    "OnBuild": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "OpenStdin": {
                        "type": "boolean"
                    },
                    "StdinOnce": {
                        "type": "boolean"
                    },
                    "Tty": {
                        "type": "boolean"
                    },
                    "User": {
                        "type": "string"
                    },
                    "Volumes": {
                        "nullable": true,
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "WorkingDir": {
                        "type": "string"
                    }
                },
                "required": [
                    "AttachStderr",
                    "AttachStdin",
                    "AttachStdout",
                    "Domainname",
                    "Hostname",
                    "Image",
                    "OpenStdin",
                    "StdinOnce",
                    "Tty",
                    "User",
                    "WorkingDir"
                ]
            },
            "GraphDriver": {
                "type": "object",
                "properties": {
                    "Data": {
                        "$ref": "#/components/schemas/GraphDriverData"
                    },
                    "Name": {
                        "type": "string"
                    }
                },
                "required": [
                    "Data",
                    "Name"
                ]
            },
            "GraphDriverData": {
                "type": "object",
                "properties": {
                    "LowerDir": {
                        "type": "string"
                    },
                    "MergedDir": {
                        "type": "string"
                    },
                    "UpperDir": {
                        "type": "string"
                    },
                    "WorkDir": {
                        "type": "string"
                    }
                },
                "required": [
                    "LowerDir",
                    "MergedDir",
                    "UpperDir",
                    "WorkDir"
                ]
            }
        }
    }
}
Originally created by @Skerga on GitHub (Oct 15, 2025). Original GitHub issue: https://github.com/games-on-whales/wolf/issues/274 This is currently more of a reminder, but i hope that the following problems with the OpenApi spec can be mended. Currently I'm trying to get NSwag to generate a working Client for the WolfApi just from the OpenApi.json the Api returns. In the process I've identified some problems that I'm currently bypass by doing some major Regex replacement. - The nullable description is incorrect, possible fix changing the used library function/implementing a custom one that generates valid syntax [Here](https://github.com/getml/reflect-cpp/blob/265b69355917c1eba0149cbbfbe2b7f245859c81/src/rfl/json/to_schema.cpp#L261-L262) - Adding the responses for `v1/utils/get-icon` and `/api/v1/docker/images/inspect` - I've found no simple way to have `runner` be both `cmd` or `docker`, currently I've removed the `cmd` option. - The `Types` have hideous and long names, I've removed most of the offender via Regex. - Maybe we should define a 204 response for the `/api/v1/docker/images/inspect` endpoint. that way an empty response would be valid and its not automatically an Exception to not find an Image. Here the RegexReplace calls I'm currently using (c# syntax "" escapes " in an @ string). I was modifying the spec directly at [this point](https://github.com/games-on-whales/wolf/blob/2984c2bb96e412c2509054415ad0b05c31c5c555/src/moonlight-server/api/openapi.cpp#L98) but the building time for wolf was taking too long for me so I've put them in the c# Source Generator. ``` // replaces all type null with nullable:true, needs more cleanup after text = Regex.Replace(text, """{"type":"null"}""", """{"nullable":true}"""); // removes anyOf if only one type and nullable:true are inside. text = Regex.Replace(text, @"""anyOf"":\[{""type"":""([aA-zZ]+)""},{""nullable"":true}]", @"""type"":""$1"",""nullable"":true"); // remove empty required arrays text = Regex.Replace(text, @",\""required\"":\[]", ""); // change number string to number value text = Regex.Replace(text, @"""(min|max)Items"":""([0-9]+)""", @"""$1Items"":$2"); // patching in the response content for get-icon text = Regex.Replace(text, @"(v1\/utils\/get-icon"":{.*?,""responses"":{)}", @"$1""200"":{""content"":{""image/png"":{""schema"":{""type"":""string"",""format"":""binary""}}},""description"":""""}}"); // force runner to be docker runner only, multiple different objects aren't possible text = Regex.Replace(text, @"(""runner"":{""anyOf"":\[.*?])}", @"""runner"":{""$ref"":""#/components/schemas/wolf__config__AppDocker__tagged""}"); // removes the enum from type, since only docker is used here. text = Regex.Replace(text, @"""type"":{""type"":""string"",""enum"":\[.*?]}", @"""type"":{""type"":""string""}"); // Make PartialClientSettings correctly optional text = Regex.Replace(text, @"""(client_settings|settings)"":{""description"":"".*?"",""anyOf"":\[{""\$ref"":""(.*?)""},{""nullable"":true}]}", @"""$1"":{""nullable"":true,""allOf"":[{""$ref"":""$2""}]}"); // fixes optional Pin fields, since they shouldn't be "anyOf" text = Regex.Replace(text, @"""anyOf"":\[{""type"":""array"",""items"":{""type"":""integer""}},{""nullable"":true}]", @"""type"":""array"",""items"":{""type"":""integer""},""nullable"":true"); // sanitize reflection type names. text = Regex.Replace(text, @"rfl__Reflector_wolf__core__events__(.*?)___ReflType", @"$1"); // sanitize endpoint return type names text = Regex.Replace(text, @"wolf__api__(.*?)""", @"$1"""); // sanitize events type names text = Regex.Replace(text, @"wolf__core__events__(.*?)""", @"$1"""); ``` I've also written a separate OpenApi file to define the `/api/v1/docker/images/inspect` endpoint: ``` { "openapi": "3.1.0", "info": { "title": "Wolf API", "description": "API for the Wolf server", "version": "0.1" }, "servers": [ { "url": "http://localhost/", "description": "Local development server" } ], "paths": { "/api/v1/docker/images/inspect": { "get": { "parameters": [ { "in": "query", "name": "image_name", "schema": { "type": "string" } } ], "summary": "Inspect a Docker image and returns the full JSON response as is from the Docker APIs at /images/{image_name}/json expects image_name as a query parameter.", "description": "", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DockerInspect" } } }, "description": "" }, "404": { "description": "Image not found" } } } } }, "components": { "schemas": { "DockerInspect": { "type": "object", "properties": { "Id": { "type": "string" }, "RepoTags": { "type": "array", "items": { "type": "string" } }, "RepoDigests": { "type": "array", "items": { "type": "string" } }, "Parent": { "type": "string" }, "Comment": { "type": "string" }, "Created": { "type": "string" }, "ContainerConfig": { "$ref": "#/components/schemas/ContainerConfig" }, "DockerVersion": { "type": "string" }, "Author": { "type": "string" }, "Architecture": { "type": "string" }, "Os": { "type": "string" }, "Size": { "type": "number", "format": "int64" }, "VirtualSize": { "type": "number", "format": "int64" }, "GraphDriver": { "$ref": "#/components/schemas/GraphDriver" }, "RootFS": { "$ref": "#/components/schemas/RootFS" }, "Metadata": { "nullable": true, "type": "object", "additionalProperties": { "type": "string" } }, "Config": { "$ref": "#/components/schemas/Config" } } }, "ContainerConfig": { "type": "object", "properties": { "Hostname": { "type": "string" }, "Domainname": { "type": "string" }, "User": { "type": "string" }, "AttachStdin": { "type": "boolean" }, "AttachStdout": { "type": "boolean" }, "AttachStderr": { "type": "boolean" }, "Tty": { "type": "boolean" }, "OpenStdin": { "type": "boolean" }, "StdinOnce": { "type": "boolean" }, "Env": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Cmd": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Image": { "type": "string" }, "Volumes": { "nullable": true, "type": "array", "items": { "type": "string" } }, "WorkingDir": { "type": "string" }, "Entrypoint": { "nullable": true, "type": "array", "items": { "type": "string" } }, "OnBuild": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Labels": { "nullable": true, "type": "object", "additionalProperties": { "type": "string" } } }, "required": [ "Hostname", "Domainname", "User", "AttachStdin", "AttachStdout", "AttachStderr", "Tty", "OpenStdin", "StdinOnce", "Image", "WorkDir" ] }, "RootFS": { "type": "object", "properties": { "Type": { "type": "string" }, "Layers": { "type": "array", "items": { "type": "string" } } } }, "Config": { "type": "object", "properties": { "AttachStderr": { "type": "boolean" }, "AttachStdin": { "type": "boolean" }, "AttachStdout": { "type": "boolean" }, "Cmd": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Domainname": { "type": "string" }, "Entrypoint": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Env": { "nullable": true, "type": "array", "items": { "type": "string" } }, "Hostname": { "type": "string" }, "Image": { "type": "string" }, "Labels": { "nullable": true, "type": "object", "additionalProperties": { "type": "string" } }, "OnBuild": { "nullable": true, "type": "array", "items": { "type": "string" } }, "OpenStdin": { "type": "boolean" }, "StdinOnce": { "type": "boolean" }, "Tty": { "type": "boolean" }, "User": { "type": "string" }, "Volumes": { "nullable": true, "type": "array", "items": { "type": "string" } }, "WorkingDir": { "type": "string" } }, "required": [ "AttachStderr", "AttachStdin", "AttachStdout", "Domainname", "Hostname", "Image", "OpenStdin", "StdinOnce", "Tty", "User", "WorkingDir" ] }, "GraphDriver": { "type": "object", "properties": { "Data": { "$ref": "#/components/schemas/GraphDriverData" }, "Name": { "type": "string" } }, "required": [ "Data", "Name" ] }, "GraphDriverData": { "type": "object", "properties": { "LowerDir": { "type": "string" }, "MergedDir": { "type": "string" }, "UpperDir": { "type": "string" }, "WorkDir": { "type": "string" } }, "required": [ "LowerDir", "MergedDir", "UpperDir", "WorkDir" ] } } } } ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#192
No description provided.