ad-on-is / adonis-autoswagger

Auto-Generate swagger docs for AdonisJS
MIT License
129 stars 41 forks source link

Feature Request: Generate Swagger Request Bodies from Validator Classes #35

Closed HasanAshab closed 5 months ago

HasanAshab commented 10 months ago

Hello,

I am currently working on a project using AdonisJS and I am using the adonis-autoswagger package to generate Swagger documentation for my API. I have noticed that while adonis-autoswagger provides a lot of flexibility and control over the generated documentation, it does not currently support generating Swagger request bodies directly from validator classes.

This limitation has led to some redundancy in our codebase, as we have to manually define the structure of the request body in both the validator class and the Swagger documentation. This not only increases the maintenance effort but also makes the code harder to understand and manage. This will keep our docs sync the code base

I believe that adding support for generating Swagger request bodies directly from validator classes would greatly enhance the usability and efficiency of adonis-autoswagger. It would allow us to define the structure of the request body in one place (the validator class), and then automatically generate the corresponding Swagger documentation.

Here's a rough idea of how this could work:


class LoginValidator {
  // generate the body using this property
  public schema = schema.create({
     email: schema.string(),
     password: schema.string()
  });
}

/**
 * @api {post} /login login a new user
 * @requestBody <LoginValidator>  // it will be equal to { email: string, password: string }
 */
Route.post('/login', async ({ request }) => {
 const user = await request.validate(LoginValidator)
 // ... rest of my code
})

In this example, the LoginValidator class defines the structure of the request body. We could then use this information to automatically generate the corresponding Swagger documentation for the /login endpoint.

I would appreciate if you could consider this feature request and let me know if there are any plans to implement it in the future. Alternatively, I would be happy to contribute to the development of this feature if you need help.

Thank you for considering this request.

Best regards, Hasan Ashab

ad-on-is commented 10 months ago

Hi, thx for the suggestion. This would be a nice feature indeed.

For this, I'd need to parse the Validators/*.ts (like we already do with the models) so this should be easy enough. Unfortunately, I don't have the time, but PRs are more than welcome.

For now, you can use the only() or without() notations. Based on your schema, I suppose you have a model User, that has an email and passwor field. You can use <User>.only(email, password) or if the Validator has more fields, you can also use <User>.without(avatar).

HasanAshab commented 10 months ago

Recently, i got work. so please anyone reading this issue, contribute if you can

Thanks in advance

ad-on-is commented 7 months ago

@HasanAshab You can however start working on this one if you'd like πŸ˜…, or do you think that the only() and without() notations are sufficient enough, so I can close it?

HasanAshab commented 7 months ago

Not sure, but I will start on it in some weeks Inshaallah :)

ad-on-is commented 7 months ago

May Allah bless you and give you the strength and patience πŸ˜…

HasanAshab commented 7 months ago

I can implement this by traversing validator properties but can't parse from text data πŸ˜…. It's pretty complex

ad-on-is commented 7 months ago

maybe the validatior has some method that returns a nicer object that can be easily parsed, in combination with dynamic imports using eval().

thinking of smth like this

HasanAshab commented 7 months ago

Oh, I got the map

HasanAshab commented 7 months ago

@ad-on-is

const v = eval(import exampleValidator from currentfile.ts).then()

This line cause

SyntaxError: Cannot use import statement outside a module

What about something like

const validatorsDir = pkgJson.imports['#validators/*']
const paths = await AutoSwagger.default.getFiles(validatorsDir)
for(let path of paths) {
  path = path
    .replace('.ts', '')
    .replace('#validators', validatorsDir)

const validators = await import(path)

console.log(validators)
}
ad-on-is commented 7 months ago

@HasanAshab My example was just pseudo-code πŸ˜…, but yeah something along these lines looks good.

HasanAshab commented 7 months ago

@ad-on-is I can make schema from vine schema. But found no way to make from compiled validators :/

ad-on-is commented 7 months ago

I'm not familiar with vinejs, but what do you get if you try to validate purposely wrong data? do you get any useful structure?

validator.validate(null)
HasanAshab commented 7 months ago

Great! I can extract required field names from error obj

[
    {                                                                   message: 'email field is required',                               rule: 'required',
      field: 'email'
    },
    {
      message: 'username field is required',
      rule: 'required',                                                 field: 'username'
    },
    {                                                                   message: 'password field is required',
      rule: 'required',
      field: 'password'                                               }
  ]

And also can extract their types by trying with all data types one by one.

But the problem is how to extract optional fields? have any Idea?

ad-on-is commented 7 months ago

by trying with all data types one by one.

I think this might be too dirty and spaghetti. Is there a cleaner solution?

Does the validator provide any other useful methods or fields? does it provide the schema maybe?

HasanAshab commented 7 months ago

The validator don't save schema as prop when its compiled. Also found no cleaner solution.

Take a look on VineValidator

ad-on-is commented 7 months ago

Yeah... I've been looking into it for the past few hours, and there's really no way to get to the schema itself.

I think the only way is to parse everything between vine.compile() since you already got that working

// βœ… supported
vine.compile(vine.object({}))

// ❌  not supported
const schema = vine.object({})
vine.compile(schema)

What do you think?

Edit: I also tried playing with the custom error reporter, but with no luck.

HasanAshab commented 7 months ago

But it may be quite difficult to parse raw text between vine.compile().

This may not work for some edge cases. Also dynamic validators would not be supported.

ad-on-is commented 7 months ago

You're right. I guess we might ditch this one after all, since it's technically impossible.

Or, as a final resort, I could ask the folks at adonisJS, whether they could provide a toJSON() function to the compiled validator, so we could get the map.

HasanAshab commented 7 months ago

Yeah please ask, I will wait for toJSON() to be added :)

ad-on-is commented 7 months ago

https://github.com/adonisjs/core/discussions/4480

Fingers crossed! 🀞🏻

ad-on-is commented 7 months ago

@HasanAshab

Nice, now that we get a toJSON(), I'd suggest the following implementation.

// autoswagger.ts
// -----------------------------
 private async getSchemas() {
    let schemas = {
      Any: {
        description: "Any JSON object not defined as schema",
      },
    };

    schemas = {
      ...schemas,
      ...(await this.getInterfaces()),
      ...(await this.getModels()),
      ...(await this.getValidators()), // convert the validator.toJSON() to the same format as models/interfaces do
    };

    return schemas;
  }

private async getValidators() {

/* 
- read files
- use ValidatorParser for parsing
- return something like this 

{exampleValidator: {
type: "object",
properties: this.validatorParser.parse(dataFromFile)
description: "Validator"
},
otherValidator: {
type: "object",
properties: this.validatorParser.parse(datafromFile)
description: "Validator"
}
}
*/

}

// parser.ts
// -----------------------------
export class ValidatorParser {
  // do your magic here
}

And the jsdoc notation should than work automatically with something like this

/* @requestBody <exampleValidator>
HasanAshab commented 7 months ago

I got it :) πŸ‘

ad-on-is commented 7 months ago

Oh, one more thing. Do you think it would be easy enough to get the errors of the validator as a response body?

Example (pseudo-code)

/**
   * @create
   * @requestBody <exampleValidator>
   * @responseBody 200 - <User>
   * @responseBody 422 - <exampleValidator.error>
   */

async create() {}

end then do something like this


let errors = []
try {
exampleValidator.validate({})
}catch(e) {
errors = e.message
}
HasanAshab commented 7 months ago

Thats good idea! I can generate multiple error response schemas from a validator, but does AutoSwagger supports multiple schema on same status code??

ad-on-is commented 7 months ago

No it does not support it, but I think it's sufficient enough just to have the schema of the error response as it is always the same.

HasanAshab commented 7 months ago

Okay and how can we know the exact type of literal?

ad-on-is commented 7 months ago
async index() {
    try {
      await testValidator.validate({
        name: 'John Doe',
        email: '',
      })
    } catch (e) {
      console.log(e.messages)
    }
    return {
      hello: 'world',
    }
  }

This is what I get as an output of console.log(e.messages)

[
  {
    message: 'The boing field must be defined',
    rule: 'required',
    field: 'boing'
  },
  {
    message: 'The title field must be defined',
    rule: 'required',
    field: 'title'
  },
  {
    message: 'The slug field must be defined',
    rule: 'required',
    field: 'slug'
  },
  {
    message: 'The description field must be defined',
    rule: 'required',
    field: 'description'
  }
]

Done! Nothing more is needed, just so everyone reading the docs knows, "oh ok, if I provide an invalid request-body, I'll get this kind of format as an error". You know what I mean? No need to be more complex here.

HasanAshab commented 7 months ago

Yeah, I got it.

ad-on-is commented 7 months ago

The error object has also a status and code property. If you like, you can use them too.

If someone uses @responseBody 400 - <exampleValidator.error> but the status of the error is 422. It's okey to overwrite the status provided by the developer, since this is AutoSwagger πŸ˜…

HasanAshab commented 7 months ago

See schema types there is literal type for string, number, etc. Now how to know the exact type?

HasanAshab commented 7 months ago

If someone uses @responseBody 400 - <exampleValidator.error> but the status of the error is 422. It's okey to overwrite the status provided by the developer, since this is AutoSwagger πŸ˜…

I think we should respect the status provided by developer as he may customized response on exception handler, am I right?

ad-on-is commented 7 months ago

See schema types there is literal type for string, number, etc. Now how to know the exact type?

oh, that's what you mean by literal

Hmm.. I'm not exactly sure, but refs['ref1://1'].validator should be a function and hopefully provide more info.

ad-on-is commented 7 months ago

If someone uses @responseBody 400 - <exampleValidator.error> but the status of the error is 422. It's okey to overwrite the status provided by the developer, since this is AutoSwagger πŸ˜…

I think we should respect the status provided by developer as he may customized response on exception handler, am I right?

Yes, you're right. The devs should be responsible for setting a proper status code, in case they override it in the controller or elsewhere.

HasanAshab commented 7 months ago

How can I play around vinejs latest commit

ad-on-is commented 7 months ago

How can I play around vinejs latest commit

I just did pnpm i https://github.com/vinejs/vine#develop and it seems to work. No need to use a specific commit, just the develop branch is sufficient

HasanAshab commented 7 months ago

Oh, thanks :)

vithalreddy commented 7 months ago

any progress on this?

Happy to help with testing this on big code base.

ad-on-is commented 6 months ago

Hey @HasanAshab, how is it going with this one? Any progress?

HasanAshab commented 6 months ago

@ad-on-is @vithalreddy Sorry guys. I just migrated to Django :/.

If anybody can, please contribute πŸ™

ad-on-is commented 6 months ago

@HasanAshab ... all good, no pressure πŸ‘

would you mind pushing a PR, with the state you currently have, so I or someone else can pick up where you left off

HasanAshab commented 6 months ago

I have no statable state. I just played with .toJSON() and some planning written on paper πŸ˜…

ad-on-is commented 6 months ago

Oh ok... I thought you were much further, with the dynamic imports, etc...

RoyalBoogie commented 6 months ago

Hi ! Unfortunately I can't help but I'll love to have this feature so I give you some strength ! Thanks btw for this package !

antoninpire commented 5 months ago

This feature would indeed be really cool, and I would not mind giving it a go. However there seems to be an issue with the toJSON of the vine validators: all primary types are considered literal. That means strings, numbers, booleans, enums etc are marked as "type": "literal" in the json returned.

ad-on-is commented 5 months ago

@antoninpire How did you get the latest vine working? or did they merge it into main?

antoninpire commented 5 months ago

@ad-on-is I think they merged it into main, I didn't have to use a specific branch, the toJSON method was there

ad-on-is commented 5 months ago

@antoninpire Ooh cool, I will give it a shot then.

ad-on-is commented 5 months ago

Fixed in 3.45.0. I tried to do my best here. Feel free to test and report any issues or feedback.

Usage as always...

testValidator.ts


export const testValidator = vine.compile(
  vine.object({
    boing: vine.object({
      blub: vine.string(),
    }),
    title: vine.string().minLength(10).maxLength(80),
    mail: vine.string().email(),
    someArr: vine.array(vine.string()),
    somebool: vine.boolean(),
    complArray: vine.array(
      vine.object({
        best: vine.string(),
        test: vine.array(
          vine.object({
            final: vine.string(),
          })
        ),
      })
    ),
    enumChoice: vine.enum(['foo', 'bar']),
    description: vine.string().trim().escape(),
    data: vine.object({
      bar: vine.number().max(10).min(5),
      arr: vine.array(vine.number().min(10).max(20)),
      regex: vine.string().regex(/^[a-z]+$/),
      baz: vine.enum(['foo', 'bar']).nullable(),
      opts: vine.string().optional(),
    }),
  })
)

... and then...


@requestBody <testValidator>
antoninpire commented 5 months ago

Awesome! Thanks a lot