Source code for flask_jsonrpc.contrib.openrpc.methods

# Copyright (c) 2024-2025, Cenobit Technologies, Inc. http://cenobit.es/
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# * Neither the name of the Cenobit Technologies nor the names of
#    its contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations

import typing as t
from functools import lru_cache
from collections import OrderedDict

from flask_jsonrpc.encoders import serializable
from flask_jsonrpc.contrib.openrpc import typing as st
from flask_jsonrpc.contrib.openrpc.utils import MethodExtendSchema, extend_schema

if t.TYPE_CHECKING:
    from flask_jsonrpc.site import JSONRPCSite

OPENRPC_DISCOVER_METHOD_NAME: str = 'rpc.discover'
OPENRPC_DISCOVER_SERVICE_METHOD_TYPE: str = 'method'


def _openrpc_discover_method(
    jsonrpc_sites: list[JSONRPCSite], *, openrpc_schema: st.OpenRPCSchema
) -> t.Callable[..., st.OpenRPCSchema]:
    """Create a cached OpenRPC discover method.

    Args:
        jsonrpc_sites (list[flask_jsonrpc.site.JSONRPCSite]): List of JSON-RPC site instances.
        openrpc_schema (flask_jsonrpc.contrib.openrpc.typing.OpenRPCSchema): The OpenRPC schema instance.

    Returns:
        typing.Callable[..., flask_jsonrpc.contrib.openrpc.typing.OpenRPCSchema]: The cached OpenRPC discover method.
    """

    @lru_cache
    @extend_schema(
        name=OPENRPC_DISCOVER_METHOD_NAME,
        description='Returns an OpenRPC schema as a description of this service',
        params=[],
        result=st.ContentDescriptor(
            name='OpenRPC Schema',
            schema_=st.Schema(ref='https://raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json'),  # pyright: ignore
        ),
    )
    def cached_openrpc_discover_method() -> st.OpenRPCSchema:
        jsonrpc_site = jsonrpc_sites[0]
        service_describe_methods = OrderedDict(
            (name, (method_describe, jsonrpc_site.view_funcs[name]))
            for name, method_describe in jsonrpc_site.describe().methods.items()
        )
        for jsonrpc_site in jsonrpc_sites[1:]:
            service_describe_methods.update(
                OrderedDict(
                    (name, (method_describe, jsonrpc_site.view_funcs[name]))
                    for name, method_describe in jsonrpc_site.describe().methods.items()
                    # To ensure that has only one rpc.* method, the others will be disregarded.
                    if not name.startswith('rpc.')
                )
            )

        for name, (method_describe, view_func) in service_describe_methods.items():
            fn_openrpc_method_schema: MethodExtendSchema = t.cast(
                MethodExtendSchema, getattr(view_func, 'openrpc_method_schema', MethodExtendSchema())
            )
            method_schema: dict[str, t.Any] = {
                'name': fn_openrpc_method_schema.name or name,
                'description': fn_openrpc_method_schema.description or method_describe.description,
                'params': [],
                'result': {
                    'name': 'default',
                    'schema': {'type': st.SchemaDataType.from_rpc_describe_type(method_describe.returns.type)},
                },
            }
            method_params_schema: list[dict[str, t.Any]] = []
            for param in method_describe.params:
                method_params_schema.append(
                    {
                        'name': param.name,
                        'schema': {'type': st.SchemaDataType.from_rpc_describe_type(param.type)},
                        'required': param.required or None,
                    }
                )
            method_schema['params'] = method_params_schema
            method_schema_merged = st.Method(
                **{**serializable(method_schema), **serializable(fn_openrpc_method_schema)}
            )
            openrpc_schema.methods.append(method_schema_merged)
        return openrpc_schema

    return cached_openrpc_discover_method  # type: ignore[no-any-return]  # pyright: ignore


[docs] def openrpc_discover_method( jsonrpc_sites: list[JSONRPCSite], *, openrpc_schema: st.OpenRPCSchema | None = None ) -> t.Callable[..., st.OpenRPCSchema]: """Create an OpenRPC discover method. Args: jsonrpc_sites (list[flask_jsonrpc.site.JSONRPCSite]): List of JSON-RPC site instances. openrpc_schema (flask_jsonrpc.contrib.openrpc.typing.OpenRPCSchema | None): The OpenRPC schema instance. If None, a default schema will be created. Returns: typing.Callable[..., flask_jsonrpc.contrib.openrpc.typing.OpenRPCSchema]: The OpenRPC discover method. See Also: :func:`flask_jsonrpc.contrib.openrpc._openrpc_discover_method` """ if openrpc_schema is None: jsonrpc_site = jsonrpc_sites[0] jsonrpc_service_describe = jsonrpc_site.describe() openrpc_schema = st.OpenRPCSchema( info=st.Info(title=jsonrpc_service_describe.name, version='0.0.1'), servers=st.Server(name='default', url=jsonrpc_service_describe.servers[0].url), ) return _openrpc_discover_method(jsonrpc_sites, openrpc_schema=openrpc_schema)