jhthorsen / mojolicious-plugin-openapi

OpenAPI / Swagger plugin for Mojolicious
54 stars 44 forks source link

Nested $ref not works in body parameters (issue?) #64

Closed ghost closed 6 years ago

ghost commented 6 years ago

Hi Jan,

I found the issue with nested $ref in body parameters... I can't check the "customer" schema that is the part of the "order" schema...

Here is the example api.json:

{
  "swagger" : "2.0",
  "info" : { "version": "0.1", "title" : "Orders" },
  "schemes" : [ "http" ],
  "basePath" : "/",
  "paths" : {
    "/order" : {
      "post" : {
        "x-mojo-to" : "testcontroller#order",
        "parameters" : [
            { "in": "body", "name": "body", "schema": { "$ref": "#/definitions/customer" } }
        ],
        "responses" : {
          "200": {
            "description": "New order response",
            "schema": { "type": "object" }
          }
        }
      }
    }
  },
  "definitions": {
    "order": {
      "type": "object",
      "description": "New order data",
      "required": [ "customer" ],
      "properties": {
        "customer": { "$ref": "#/definitions/customer" }
      }
    },
    "customer": {
      "type": "object",
      "description": "Customer personal data",
      "required": [ "first_name", "last_name", "email" ],
      "properties": {
        "first_name": { "type": "string" },
        "last_name": { "type": "string" },
        "email": { "type": "string" },
        "phone": { "type": "string" }
      }
    }
  }
}

Test service:

#!/usr/bin/env perl

package TestAPI::Controller::Testcontroller;
use Mojo::Base 'Mojolicious::Controller';

sub order
{
    my $c = shift;

    $c->app->log->warn('Body: ' . $c->req->body);

    return unless ($c->openapi->valid_input);

    $c->render(openapi => { status => 'ok' });
}

#
package TestAPI;
use Mojo::Base 'Mojolicious';

sub startup
{
    shift->plugin( OpenAPI => { url => 'api.json' });
}

#
package main;
use strict;
use warnings;

require Mojolicious::Commands;
Mojolicious::Commands->start_app('TestAPI');

Example query:

curl -X POST -d '{"customer":{"first_name":"a","last_name":"b","email":"test@test.org"}}' http://127.0.0.1:3000/order

Response:

[Thu Feb  8 11:39:51 2018] [debug] POST "/order"
[Thu Feb  8 11:39:51 2018] [debug] Routing to controller "TestAPI::Controller::Testcontroller" and action "order"
[Thu Feb  8 11:39:51 2018] [warn] Body: {"customer":{"first_name":"a","last_name":"b","email":"test@test.org"}}
[Thu Feb  8 11:39:51 2018] [debug] Your secret passphrase needs to be changed
[Thu Feb  8 11:39:52 2018] [warn] OpenAPI <<< POST /order [{"message":"Missing property.","path":"\/body\/email"},{"message":"Missing property.","path":"\/body\/first_name"},{"message":"Missing property.","path":"\/body\/last_name"}]
[Thu Feb  8 11:39:52 2018] [debug] 400 Bad Request (0.00217000000000001s, 460.829/s)

So the question is: is it possible to use nested $ref by OpenAPI or it is the module issue?

jhthorsen commented 6 years ago

Are you sure your $ref is right..? Shouldn't it be "#/definitions/order" instead of "#/definitions/customer"..?

ghost commented 6 years ago

Yes, I am sure. Because I need a case to create a customer and a service in one request. So I need to check something like this:

{"customer":{"first_name":"a","last_name":"b","email":"test@test.org"},"service":{"product_id":1,"product_props":{"prop_a":1,"prop_b":2}}}

Is it possible?

jhthorsen commented 6 years ago

But the "customer" definitions does not have a "customer" property, while the "order" do have:

"order": { <----
  "type": "object",
  "description": "New order data",
  "required": [ "customer" ], <----
  "properties": {
    "customer": { "$ref": "#/definitions/customer" } <----
  }
}
ghost commented 6 years ago

Why I need "customer" property in "customer" definition?

I need to create Order that contain customer object. So my query is correct. Customer definition describe only Customer object. Order definition contain customer object...

Maybe this part of real code can help you to understand what I want?

sub order
{
    my $c = shift;
    $c->app->log->warn('Body: ' . $c->req->body);
    return unless ($c->openapi->valid_input);

    my $params = $c->validation->output;

    my $order = Order->new(
         customer => Customer->new($params->{'customer'}),
         service => Service->new($params->{'service'})
    );

    $c->render(openapi => { status => 'ok', order => $order });
}
curl -X POST -d '{"customer":{"first_name":"a","last_name":"b","email":"test@test.org"},"service":{"product_id":1,"product_props":{"prop_a":1,"prop_b":2}}}' http://127.0.0.1:3000/order

So the request should create new customer, new service and new order by one request...

mohawk2 commented 6 years ago

@dshadow I think this is the heart of the matter:

    "/order" : {
      "post" : {
...
        "parameters" : [
            { "in": "body", "name": "body", "schema": { "$ref": "#/definitions/customer" } }
        ],

You are describing an API with an endpoint called /order, whose input parameter you are saying must be a customer. This does not make sense to me, and I believe is also why @jhthorsen asked whether you are sure you are right.

I humbly suggest that you think again about whether you really intend to pass a customer when posting to /order, while giving on your curl command line something that matches an order (e.g. it has a "customer" field).

jberger commented 6 years ago

I agree with @jhthorsen and @mohawk2, your post body definition is going to be some new object which has customer and service properties. Each of those can then use your existing schemas to validate those portions.

jberger commented 6 years ago

For completeness:

{
  "swagger" : "2.0",
  "info" : { "version": "0.1", "title" : "Orders" },
  "schemes" : [ "http" ],
  "basePath" : "/",
  "paths" : {
    "/order" : {
      "post" : {
        "x-mojo-to" : "testcontroller#order",
        "parameters" : [
            { "in": "body", "name": "body", "schema": { "$ref": "#/definitions/order" } }
        ],
        "responses" : {
          "200": {
            "description": "New order response",
            "schema": { "type": "object" }
          }
        }
      }
    }
  },
  "definitions": {
    "order": {
      "type": "object",
      "description": "New order data",
      "required": [ "customer" ],
      "properties": {
        "customer": { "$ref": "#/definitions/customer" },
        "service": { "$ref": "#/definitions/service" }
      }
    },
    "customer": {
      "type": "object",
      "description": "Customer personal data",
      "required": [ "first_name", "last_name", "email" ],
      "properties": {
        "first_name": { "type": "string" },
        "last_name": { "type": "string" },
        "email": { "type": "string" },
        "phone": { "type": "string" }
      }
    },
    "service": { ... }
  }
}