generate_schema_config.py 7.96 KB
Newer Older
1 2
from formats import message_header
from formats.mission_plan import mission_plan_schema
3
from formats.mission_plan_encoded import mission_plan_encoded_schema
4
from formats.observation import observation_schema
5
from formats.observation_encoded import observation_encoded_schema
6
from formats.planning_configuration import planning_configuration_schema
7
from formats.platform_status import platform_status_schema
8
from formats.platform_status_encoded import platform_status_encoded_schema
9 10
from formats.survey import survey_schema
from formats.survey_encoded import survey_encoded_schema
11
from formats.acknowledgement import acknowledgement_schema
12
from formats.alert import alert_schema
13 14 15 16

from flasgger import Swagger
from flask import Flask

17 18
import argparse
import json
19 20
import os

21

22 23 24 25
# Enable running on domain sub-path
URL_PREFIX = os.getenv("URL_PREFIX", "")
# Allow env override of default host
FLASK_HOST = os.getenv("FLASK_HOST", "localhost")
Dan Jones's avatar
Dan Jones committed
26 27
# Allow env override of default port
FLASK_PORT = os.getenv("FLASK_PORT", 5000)
28 29
# Switch on debug mode if env var is truthy
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "False").lower() in ("true", "1", "t")
30

31 32 33 34
swagger_config = {
    "openapi": "3.0.2",
    "swagger_ui": True,
    "specs_route": "/",
35
    "info": {
36 37
        "title": "SoAR Backbone Message Formats",
        "version": "1.0",
38
        "description": "SoAR message protocol in schemas",
39 40 41 42 43 44 45
    },
    "specs": [
        {
            "endpoint": "swagger",
            "route": "/soar_protocol.json",
        }
    ],
46
    "url_prefix": URL_PREFIX,
47 48 49 50 51
    "paths": {},
    "components": {
        "schemas": {
            "MESSAGE": {
                "type": "object",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
52 53 54
                "description": "Full message definition with"
                + " message-metadata in `header` and different"
                + " message type schemas under `payload`",
55 56 57 58
                "properties": {
                    "header": {
                        "$ref": "#/components/schemas/header",
                    },
59
                    "payload": {"$ref": "#/components/schemas/payload"},
60 61 62 63 64 65
                },
                "required": ["header", "payload"],
            },
            "payload": {
                "discriminator": {
                    "propertyName": "message_type",
66
                    "mapping": {
67
                        "alert": "#/components/schemas/alert",
68
                        "mission_plan": "#/components/schemas/mission_plan",
69 70
                        "mission_plan_encoded": "#/components/schemas/"
                        + "mission_plan_encoded",
71
                        "observation": "#/components/schemas/observation",
72 73
                        "observation_encoded": "#/components/schemas/"
                        + "observation_encoded",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
74 75
                        "planning_configuration": "#/components/schemas/"
                        + "planning_configuration",
76
                        "platform_status": "#/components/schemas/platform_status",
77 78
                        "platform_status_encoded": "#/components/schemas/"
                        + "platform_status_encoded",
79
                        "acknowledgement": "#/components/schemas/acknowledgement",
80
                        "survey": "#/components/schemas/survey",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
81
                        "survey_encoded": "#/components/schemas/" + "survey_encoded",
82 83
                    },
                },
84
                "oneOf": [
85
                    {"$ref": "#/components/schemas/alert"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
86 87
                    {"$ref": "#/components/schemas/acknowledgement"},
                    {"$ref": "#/components/schemas/mission_plan"},
88
                    {"$ref": "#/components/schemas/mission_plan_encoded"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
89
                    {"$ref": "#/components/schemas/observation"},
90
                    {"$ref": "#/components/schemas/observation_encoded"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
91 92
                    {"$ref": "#/components/schemas/planning_configuration"},
                    {"$ref": "#/components/schemas/platform_status"},
93
                    {"$ref": "#/components/schemas/platform_status_encoded"},
94 95
                    {"$ref": "#/components/schemas/survey"},
                    {"$ref": "#/components/schemas/survey_encoded"},
96
                ],
97 98 99
            },
            "header": message_header,
            "mission_plan": mission_plan_schema,
100
            "mission_plan_encoded": mission_plan_encoded_schema,
101
            "observation": observation_schema,
102
            "observation_encoded": observation_encoded_schema,
103
            "planning_configuration": planning_configuration_schema,
104
            "platform_status": platform_status_schema,
105
            "platform_status_encoded": platform_status_encoded_schema,
106 107
            "survey": survey_schema,
            "survey_encoded": survey_encoded_schema,
108
            "acknowledgement": acknowledgement_schema,
109
            "alert": alert_schema,
110 111 112 113
        }
    },
}

Dan Jones's avatar
Dan Jones committed
114 115

def configure_flask(swagger_config):
116
    """
Dan Jones's avatar
Dan Jones committed
117
    Setup a flask app, load flasgger
118

Dan Jones's avatar
Dan Jones committed
119
    and then patch to remove invalid
120 121
    definitions:{} object
    """
122 123
    app = Flask(__name__)
    Swagger(app, config=swagger_config, merge=True)
Dan Jones's avatar
Dan Jones committed
124

125 126 127 128 129 130 131 132 133 134
    # Replace schema route to remove invalid
    # definitions: {}
    # Should be fixed if Flassger 0.9.7 is released
    #
    # The last release of flasgger was Aug 2020
    # This bug was fixed in Nov 2021
    # There is a pre-release from May 2023
    # Until the fix gets released we have to
    # remove the invalid definitions object
    # from the spec
135 136
    @app.after_request
    def after_request_decorator(response):
137
        """
Dan Jones's avatar
Dan Jones committed
138 139
        I didn't want to mess with flasgger so
        this blunt workaround that runs on every
140 141 142 143 144 145 146 147 148
        route and then checks whether it's required
        """
        is_response = type(response).__name__ == "Response"
        is_json = is_response and response.content_type == "application/json"
        if is_json:
            parsed = response.json
            if "definitions" in parsed:
                del parsed["definitions"]
            response.data = json.dumps(parsed)
149 150

        return response
Dan Jones's avatar
Dan Jones committed
151

Dan Jones's avatar
Dan Jones committed
152 153 154 155 156
    return app


def serve(swagger_config):
    """
157
    Run as local flask app on FLASK_PORT|5000
Dan Jones's avatar
Dan Jones committed
158
    """
Dan Jones's avatar
Dan Jones committed
159
    app = configure_flask(swagger_config)
160
    app.run(debug=FLASK_DEBUG, host=FLASK_HOST, port=FLASK_PORT)
161 162


163 164
def compile_schema(swagger_config):
    """Extract the output schema from flasgger
Dan Jones's avatar
Dan Jones committed
165 166 167

    The only way I have found to do this is to
    use a test client to make the GET request
168 169
    for the page

Dan Jones's avatar
Dan Jones committed
170
    The function that returns the definition
171 172
    can't be called outside the flask app context
    """
Dan Jones's avatar
Dan Jones committed
173 174
    app = configure_flask(swagger_config)
    route = swagger_config["specs"][0]["route"]
175 176 177 178 179 180
    client = app.test_client()
    response = client.get(route)
    spec = response.json
    return spec


181 182 183 184
def write_schema(swagger_config, file_path):
    """
    Dump schema to specified file
    """
185 186 187
    spec = compile_schema(swagger_config)
    json_schema = json.dumps(spec, indent=2)

Dan Jones's avatar
Dan Jones committed
188
    with open(file_path, "w") as f:
189 190 191 192 193 194 195
        f.write(json_schema)


def get_options():
    """
    Parse script arguments
    """
Dan Jones's avatar
Dan Jones committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
    parser = argparse.ArgumentParser(
        description="Generate the schema",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "-s",
        "--serve",
        dest="run_flask",
        action="store_true",
        help="Run flask app",
        default=False,
    )
    parser.add_argument(
        "-f",
        "--file",
        dest="output_file",
        action="store_true",
        help="Save output to schema file",
        default=False,
    )
216
    parser.add_argument("filename", nargs="?", default="project/soar/swagger.json")
217 218 219
    args = parser.parse_args()
    config = vars(args)
    # If no flag is specified default to running the flask server
Dan Jones's avatar
Dan Jones committed
220
    if all(v is False for v in config.values()):
221
        config["run_flask"] = True
Dan Jones's avatar
Dan Jones committed
222 223
    return config

224

Dan Jones's avatar
Dan Jones committed
225 226 227
if __name__ == "__main__":
    # Parse script args
    config = get_options()
228

229 230
    # Output compiled schema
    if config.get("output_file"):
231
        write_schema(swagger_config, config.get("filename"))
232

233 234
    # Run flask app
    if config.get("run_flask"):
Dan Jones's avatar
Dan Jones committed
235
        serve(swagger_config)