open-rpc / spec

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

Feature: Re-using params for several methods #357

Open pontus-eliason opened 2 years ago

pontus-eliason commented 2 years ago

Hi!

For our methods, we have multiple common params for each method, let's call them Username and Password. So if we have 100 methods, we are for each one required to write:

"params": [
    {"$ref": "#/components/contentDescriptors/Username"},
    {"$ref": "#/components/contentDescriptors/Password"},
    {
        "name": "X",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/X"
        }
    }
]

I tried to conjure up some make-believe kind of schema that could solve this, but I'm actually stumped. Because it seems very difficult to do, since the params must be an array, and the inheritance through some sort of allOf quickly becomes clumsy.

I need this for quite a simple reason: When we generate code from the specification, it would greatly help to be able to say "All request params follow this interface containing Username and Password".

The only current solution I've been able to think of is to do something like this using specification extensions:

"contentDescriptors": [
    {
        "name": "Username",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Username"
        },
        "x-interface": "CredentialProperties"
    },
    {
        "name": "Password",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Password"
        },
        "x-interface": "CredentialProperties"
    }
]

And then alter the code generation logic in a way that makes the 2 properties be added to a custom interface, so when we receive it on the server end (in Java) we get a class RequestX which implements interface CredentialProperties.

Is there a better way of doing this?

github-actions[bot] commented 2 years ago

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

BelfordZ commented 2 years ago

@pontus-eliason firstly, I want to commend you for the great use of spec extensions!

I think you are on the right track with this. One thing that comes to mind that you could try is to run a pre-processing step on your openrpc document which would remove the x-interface key and in turn, add in the params to all the required places.

for example, if you start with

{
  methods: [{
    method: "foo",
    params: [
      {
        "name": "X",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/X"
        }
      }
    ],
    result: [],
    x-extend-params: ["CredentialProperties"]
  }],
  components: {
    contentDescriptors: {
       username: {
         "name": "Username",
         "required": true,
         "schema": {
           "$ref": "#/components/schemas/Username"
         },
         "x-interface": "CredentialProperties"
       },
       {
         "name": "Password",
         "required": true,
         "schema": {
           "$ref": "#/components/schemas/Password"
         },
         "x-interface": "CredentialProperties"
       }
    }
  }

then you'd run your spec extension pre-processor and get back:

{
  methods: [{
    method: "foo",
    params: [
      {"$ref": "#/components/contentDescriptors/Username"},
      {"$ref": "#/components/contentDescriptors/Password"},
      {
        "name": "X",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/X"
        }
      }
    ],
    result: [],
    x-extends-interface: ["CredentialProperties"]
  }],
  components: {
    contentDescriptors: {
       username: {
         "name": "Username",
         "required": true,
         "schema": {
           "$ref": "#/components/schemas/Username"
         },
         "x-interface": "CredentialProperties"
       },
       {
         "name": "Password",
         "required": true,
         "schema": {
           "$ref": "#/components/schemas/Password"
         },
         "x-interface": "CredentialProperties"
       }
    }
  }

once running through codegen, you would (presumably) get the same resulting interfaces.

Another similar approach would be to use root-level vendor extension, and create your own abstraction around applying content descriptors to specific methods, and once again, run a preprocessor on it.

If for whatever reason pre-processing is not an option, then you would need tooling to support your specification extension, or have a plugin/extension interface allowing you to add code to handle your extension.

Hopefully that helps.

BelfordZ commented 2 years ago

You could also have your specification extension make use of tags in order to group methods based on what interfaces they extend from. Then you would only need the 1 field x-interface which matches contentDescriptors with the methods that they should be added to.

BelfordZ commented 2 years ago

Something to consider: when params is byPosition (method expects an array), you would need a way to specify the order in which the params should be added/extended/inherited (what ever you wanna call that =p).