open-rpc / spec

The OpenRPC specification
https://spec.open-rpc.org
Apache License 2.0
166 stars 49 forks source link

Allow multiple input/output signatures for the same method name #367

Open steveluscher opened 1 year ago

steveluscher commented 1 year ago

Problem

In the docs for Method it is written:

The method name is used as the method field of the JSON-RPC body. It therefore MUST be unique.

Consider a JSON-RPC API method of ours called getBlock. If you pass transactionDetails: 'full' in the input then the output takes this form:

type GetBlockMethodResponse = {
  transactions: Array<{
    transaction: {
      message: VersionedMessage;
      signatures: string[];
    };
  }>;
}

If, however, you pass transactionDetails: 'accounts' then the output takes this form:

type GetBlockMethodResponse = {
  transactions: Array<{
    transaction: {
      accountsKeys: ParsedMessageAccount[],
      signatures: string[];
    };
  }>;
}

Without the ability to specify two different input/output signatures for the same method name, I don't think I can use OpenRPC to express that API.

Proposed solution

Allow multiple definitions for the same method name, and let the consumers of the specification document figure out how to either:

  1. Generate overloads when appropriate (eg. in a Typescript client)
  2. Merge the definitions into a single input/output specification that's the union of all possible inputs/outputs
github-actions[bot] commented 1 year ago

Welcome to OpenRPC! Thank you for taking the time to create an issue. Please review the guidelines

etodanik commented 6 months ago

As a fellow Solana-ite, I support @steveluscher request. That being said, the propose methods would probably provide a spec that is too ambiguous when it comes to API client generation, especially for lower level languages like C/C++.

A generator must know how to generically create the overloads, so the spec must somehow indicate how response schemas change and not leave that work up to a specific client implementation.

The way that OpenAPI 3.0 approaches it is via oneOf with a discriminator: https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/

It looks like this

components:
  responses:
    sampleObjectResponse:
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/Object1'
              - $ref: '#/components/schemas/Object2'
              - $ref: 'sysObject.json#/sysObject'
            discriminator:
              propertyName: objectType
              mapping:
                obj1: '#/components/schemas/Object1'
        obj2: '#/components/schemas/Object2'
                system: 'sysObject.json#/sysObject'
  …
  schemas:
    Object1:
      type: object
      required:
        - objectType
      properties:
        objectType:
          type: string
      …
    Object2:
      type: object
      required:
        - objectType
      properties:
        objectType:
          type: string
      …

I would imagine that something very similar is doable for OpenRPC.

etodanik commented 6 months ago

To add more context, discriminator is currently an OpenAPI-only addition which doesn't break json-schema because json-schema ignore unknown stuff.

Some discussion in OpenAPI about this exact issue: https://github.com/OAI/OpenAPI-Specification/issues/2116 https://github.com/OAI/OpenAPI-Specification/issues/403
https://github.com/OAI/OpenAPI-Specification/issues/2143 (especially)

etodanik commented 6 months ago

So in theory, this kind of OpenRPC schema would be helpful for codegen:

{
  "openrpc": "1.0.0-rc1",
  "info": {
    "version": "1.0.0",
    "title": "Petstore",
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "http://localhost:8080"
    }
  ],
  "methods": [
    {
      "name": "list_pets",
      "summary": "List all pets",
      "tags": [
        {
          "name": "pets"
        }
      ],
      "params": [
        {
          "name": "limit",
          "description": "How many items to return at one time (max 100)",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "responseType",
          "description": "Retrieve only pet id",
          "required": true,
          "schema": {
            "enum": ["full", "idOnly"]
          }
        }
      ],
      "result": {
        "name": "pets",
        "description": "A paged array of pets",
        "schema": {
          "oneOf": [
            { "$ref": "#/components/schemas/Pets" },
            { "$ref": "#/components/schemas/PetsIdOnly" }
          ],
          "discriminator": {
            "propertyName": "responseType",
            "mapping": {
              "full": "#/components/schemas/Pets",
              "idOnly": "#/components/schemas/PetsIdOnly"
            }
          }
        }
      },
      "errors": [
        {
          "code": 100,
          "message": "pets busy"
        }
      ],
      "examples": [
        {
          "name": "listPetExample",
          "description": "List pet example",
          "params": [
            {
              "name": "limit",
              "value": 1
            }
          ],
          "result": {
            "name": "listPetResultExample",
            "value": [
              {
                "id": 7,
                "name": "fluffy",
                "tag": "poodle"
              }
            ]
          }
        }
      ]
    },
    {
      "name": "create_pet",
      "summary": "Create a pet",
      "tags": [
        {
          "name": "pets"
        }
      ],
      "params": [
        {
          "name": "newPetName",
          "description": "Name of pet to create",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "newPetTag",
          "description": "Pet tag to create",
          "schema": {
            "type": "string"
          }
        }
      ],
      "examples": [
        {
          "name": "createPetExample",
          "description": "Create pet example",
          "params": [
            {
              "name": "newPetName",
              "value": "fluffy"
            },
            {
              "name": "tag",
              "value": "poodle"
            }
          ],
          "result": {
            "name": "listPetResultExample",
            "value": 7
          }
        }
      ],
      "result": {
        "$ref": "#/components/contentDescriptors/PetId"
      }
    },
    {
      "name": "get_pet",
      "summary": "Info for a specific pet",
      "tags": [
        {
          "name": "pets"
        }
      ],
      "params": [
        {
          "$ref": "#/components/contentDescriptors/PetId"
        }
      ],
      "result": {
        "name": "pet",
        "description": "Expected response to a valid request",
        "schema": {
          "$ref": "#/components/schemas/Pet"
        }
      },
      "examples": [
        {
          "name": "getPetExample",
          "description": "get pet example",
          "params": [
            {
              "name": "petId",
              "value": 7
            }
          ],
          "result": {
            "name": "getPetExampleResult",
            "value": {
              "name": "fluffy",
              "tag": "poodle",
              "id": 7
            }
          }
        }
      ]
    }
  ],
  "components": {
    "contentDescriptors": {
      "PetId": {
        "name": "petId",
        "required": true,
        "description": "The id of the pet to retrieve",
        "schema": {
          "$ref": "#/components/schemas/PetId"
        }
      }
    },
    "schemas": {
      "PetId": {
        "type": "integer",
        "minimum": 0
      },
      "Pet": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/PetId"
          },
          "name": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        }
      },
      "Pets": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/Pet"
        }
      },
      "PetsIdOnly": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/PetId"
        }
      }
    }
  }
}

Of course, this passes 'json-schema' validation because it would simply ignore unknown stuff. But it would be nice to have it as an official OpenRPC extension of json-schema rather than a free-for-all implicit abuse of validator permissiveness.