Giter Club home page Giter Club logo

actix-swagger's Introduction

openapi/openapi

Generate JavaScript or TypeScript code from Swagger/OpenAPI specification.

Install

npm install -D openapi
# or
pnpm add -D openapi
# or
yarn add -D openapi

Examples

pnpm openapi --file ../src/mocks/local-file-api.json
# or
pnpm openapi --file ../src/mocks/local-file-api.yaml
# or
pnpm openapi --file protocol://url/api.json

Usage CLI

pnpm openapi [options]

Options:
  -V, --version            output the version number
  --output-dir <path>      Path output directory js api with types (default: './api')
  --config <path>          Path to config
  --mode <type>            Mode for additional info: 'prod' | 'dev' (default: 'prod')
  --file <path>            Path to file with api (*.json, *.yaml, url)
  --authorization <value>  Auth token for get api by url (it is header for request)
  --deprecated <type>      Action for deprecated methods: 'warning' | 'ignore' | 'exception' (default: 'warning')
  --import-request         Import request code in out code
  --original-body          Build with original request body
  --ignore-description     Print description of request
  -h, --help               display help for command

Usage config in file

This package use cosmiconfig for finding config.

Config can exist next places

  • a openapi property in package.json
  • a .openapirc file in JSON or YAML format
  • a .openapirc.json file
  • a .openapirc.yaml, .openapirc.yml, or .openapirc.js file
  • a openapi.config.js file exporting a JS object
module.exports = {
  // Path to file with api (*.json, *.yaml, url)
  file: "./swagger-api.json", // string

  // Api in json (if not use option 'file', more important than path to file)
  apiJson: { ... },

  // Auth token for get api by url (it is header for request)
  authorization: "Token 123qwerty", // string

  // Path output directory js api with types
  outputDir: "./api", // string (default: "./api")

  // Mode for additional info
  mode: "prod", // "prod" | "dev" (default: "prod")

  // Action for deprecated methods
  deprecated: "warning", // "warning" | "ignore" | "exception" (default: "warning")

  // Import request code in out code
  // true — add import from `openapi/request`
  // false — embed request to `outputDir` and import from it
  // "disabled" — completely disable imporing `request`, use `templateCodeBefore`
  importRequest: true, // (default: false)

  // Build with original request body
  originalBody: true, // (default: false)

  // Ignore description of requests
  ignoreDescription: true, // default: false

  // Completely disable generating types file (.d.ts)
  disableTypesGenerate: true, // (default: false)

  /**
   * Change file name for source code
   * Also it can be a function
   * @example
   * templateFileNameCode: ({ swaggerData, changeCase }) => string,
   */
  templateFileNameCode: 'my-api.js', // (default: 'index.js')

  /**
   * Change file name for typing
   * Also it can be a function
   * @example
   * templateFileNameTypes: ({ swaggerData, changeCase }) => string,
   */
  templateFileNameTypes: 'my-api.d.ts', // (default: 'index.d.js')

  /**
   * Load presets and merge local properties to it
   * If preset created as a function, options can be passed
   * @example
   * presets: [
   *  ['my-super-openapi-preset', { passed: 'options' }],
   *  ['another-openapi-preset', { beautiful: 'options' }],
   * ]
   * If no options passed or used simple form, empty object passed to functional preset
   */
  presets: ['my-super-openapi-preset'], // (default: [])

  /**
   * Template before main block code
   * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   */
  templateCodeBefore: (extra) => "",

  /**
   * Template request code
   * @param {{
   *  name: string;
   *  method: string;
   *  url: string;
   *  isWarningDeprecated: boolean;
   *  isExistParams: boolean;
   *  defaultParams: object;
   * }} params
   * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  requestSwaggerData: { operationId: string; requestBody?: object; responses: object };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   */
  templateRequestCode: (params, extra) => "",

  /**
   * Template after maon block code
   * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   */
  templateCodeAfter: (extra) => "",

  /**
   * Template before main block types
   * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   */
  templateTypesBefore: (extra) => "",

  /**
   * Template request types
   * @param {{
   *  name: string;
   *  summary: string;
   *  description: string;
   *  countVariants: number;
   *  index: number;
   *  params: SwaggerData | null;
   *  addedParams: SwaggerData | null;
   *  result: SwaggerData | null;
   * }} params
   * * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  requestSwaggerData: { operationId: string; requestBody?: object; responses: object };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   *
   * @type {https://swagger.io/docs/specification/data-models/} SwaggerData
   */
  templateRequestTypes: (param, extra) => "",

  /**
   * Template after main block types
   * @param {{
   *  swaggerData: { info: object; paths: object; components: object; };
   *  changeCase: { paramCase: Function; camelCase: Function; pascalCase: Function; ... };
   * }} extra
   */
  templateTypesAfter: (extra) => "",
};

API

import { openapiGenerate } from "openapi";

const { code, types } = openapiGenerate({
  file: "./swagger-api.json",
});

console.log(code);
// => js code

console.log(types);
// => typescript types

More examples

Additional notes

  • If you will use this package after application created, you will have problem with generated api, because current api in your app will have different with your swagger api maybe.

How to create custom preset

  1. Create new NPM package (create directory and npm init there)
  2. Name your package with openapi-preset- prefix (ex.: openapi-preset-effector)
  3. Create index.js and set "main": "index.js" in your package.json
  4. Fill your index.js with any properties from list before
  5. Save and publish
  6. Use it like: presets: ['openapi-preset-example']

Hint: if you want to use local file as a preset, just use require.resolve: presets: [require.resolve('./local-preset')] It is works only in .js configs

Preset with options

  1. Export from your javascript file function with single argument
  2. Add valid options to your README.md
  3. Use nested array form to pass options to preset

Example preset with options

module.exports = (options) => ({
  templateRequestCode: (request, extra) =>
    options.parseBody
      ? generatorWithParser(request, extra)
      : simpleGenerator(request, extra),
});

Usage openapi.config.js:

module.exports = {
  file: "./swagger-api.json",
  presets: [
    ["openapi-preset-example", { parseBody: true }],
    [
      "openapi-preset-another",
      { requestImport: { module: "./axios-fabric", name: "axios" } },
    ],
  ],
};

Tested generation on swagger versions

  • 2.0
  • 3.0.1
  • 3.0.2

Roadmap

  • Struct generated files by tags
  • Detect nullable
  • Validate by schema
  • Combine types by

Development

How to release a new version

  1. Wait for release-drafter to generates a new draft release
  2. All PRs should have correct labels and useful titles. You can review available labels here.
  3. Update labels for PRs and titles, next manually run the release drafter action to regenerate the draft release.
  4. Review the new version and press "Publish"
  5. If required check "Create discussion for this release"

actix-swagger's People

Contributors

drevoed avatar jayvdb avatar sergeysova avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

actix-swagger's Issues

Extract header value

    pub mod parameters {
        use actix_web::{FromRequest, HttpRequest};
        use serde::Serialize;

        #[derive(Debug, Serialize, Clone)]
        struct ParseHeaderError {
            error: String,
            message: String,
        }

        fn extract_header(req: &HttpRequest, name: String) -> Result<String, ParseHeaderError> {
            let header_error = ParseHeaderError {
                error: "header_required".to_string(),
                message: format!("header '{}' is required", name),
            };

            let header = req.headers().get(name).ok_or(header_error.clone())?;
            let value = header.to_str().map_err(|_| header_error)?.to_string();
            Ok(value)
        }

        pub struct AccessToken(pub String);

        impl FromRequest for AccessToken {
            type Config = ();
            type Error = actix_web::Error;
            type Future = futures::future::Ready<Result<Self, Self::Error>>;

            #[inline]
            fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
                match extract_header(&req, "X-Access-Token".to_string()) {
                    Ok(value) => futures::future::ok(AccessToken(value)),
                    Err(reason) => match serde_json::to_string(&reason) {
                        Ok(json) => futures::future::err(actix_web::error::ErrorBadRequest(json)),
                        Err(error) => {
                            futures::future::err(actix_web::error::ErrorInternalServerError(error))
                        }
                    },
                }
            }
        }
    }

Convert generated response to `actix_swagger::Answer` through `Into`

    pub mod session_get {
        use super::responses;
        use actix_swagger::ContentType;
        use actix_web::http::StatusCode;
        use serde::Serialize;

        pub type Answer = actix_swagger::Answer<'static, Response>;

        #[derive(Debug, Serialize)]
        #[serde(untagged)]
        pub enum Response {
            Ok(responses::SessionGetSuccess),
            Unauthorized,
            Unexpected,
        }

        impl Into<Answer> for Response {
            #[inline]
            fn into(self) -> Answer {
                let status = match self {
                    Self::Ok(_) => StatusCode::OK,
                    Self::Unauthorized => StatusCode::UNAUTHORIZED,
                    Self::Unexpected => StatusCode::INTERNAL_SERVER_ERROR,
                };

                let content_type = match self {
                    Self::Ok(_) => Some(ContentType::Json),
                    Self::Unauthorized => None,
                    Self::Unexpected => None,
                };

                Answer::new(self).status(status).content_type(content_type)
            }
        }

rel #17

Parsing arguments fails

I'm trying to use it as per docs, but getting error at the stage of parsing arguments:

> cargo swagg ./AlpacaDeviceAPI_v1.yaml --out-file src/api.rs
error: Found argument './AlpacaDeviceAPI_v1.yaml' which wasn't expected, or isn't valid in this context

USAGE:
    cargo-swagg.exe [OPTIONS] <source>

For more information try --help

Am I missing something?

Discussion about implementation

https://t.me/rustlang_ru/285259 https://teleg.one/rustlang_ru/285259 (Russian)

Translated:
made a sufficient version of the code generator for actix_swagger.
Generates this: https://github.com/sergeysova/actix-swagger/blob/master/cargo-swagg/out.rs.

In principle, it's enough to write applications, I have about this kind of communication. But there is no execution of the contract of request handlers here. There is no obligation to correctly process the request body in the handler. I could not find a way to do it through actix_web::dev::Factory.

Here is a discussion issue: actix/actix-web#1339.

By actix-swagger:
I will now write a conversion of yaml structures to the code generator format. Most likely there will be a HashMap/BTreeMap bundle to correctly resolve links. The first implementation will be very stupid and will be able to work only with links in requestBody, params and so on. There will also be no support for importing from files.

What do you need help with?

  • Have a look at the code. Almost everything is written there to get the first working version.
  • It would be cool to throw tests on the code generator (called printer). I think trivial inline snapshots would be enough. Give different configuration options to input and get sane code.
  • Convert your personal openapi3 snapshots into cargo-swagg structures and see if it's enough to have or need something else. If you need something else, create an issue/pr with implementation.

Project goals:
To input the openapi3 specification, to get the generated code at the output that does not need to be corrected by hand. At the same time, you may commit it into the repo and watch changes in the guitar after manual re-generation. You can take it to a separate crate, re-generate it into ci and deploy the crate of the new version. Update it in the project when it is convenient.

How to use:

We have to figure this out:

how to get the request handler to execute the contract on the request body, query, and headers.

I was thinking of making my analogue actix_web::dev::Factory, the so-called BoundFactory. Which would prescribe the implementation for Fn. BoundFactory is a trait that requires a function to have several mandatory parameters for specific types.

In this way, handlers could have the first arguments of clear types, such as request_body: request_bodies::Example, query: session_create::Query, and the rest of the arguments would be derived types, so you can pull out the necessary things, such as actix_web::web::Data.

But I'm relying on the privacy of Handler - actix/actix-web#1339.

I did not understand how to implement BoundFactory or even achieve the goal through Handle trait (actix/actix-web#1275).

I'd be so grateful for any help.

Remove Answer<'static from route implementation

We can generate type alias. Instead of this:

pub async fn route(
    app: web::Data<crate::App>,
) -> Answer<'static, register_request::Response> {

Generate result type:

pub async fn route(
    app: web::Data<crate::App>,
) -> register_request::Answer {

Cannot use project with Actix-web 4.2.1

error[E0432]: unresolved imports `actix_web::http::header::IntoHeaderValue`, `actix_web::http::HeaderName`, `actix_web::http::HeaderValue`
  --> /home/rafal/.cargo/registry/src/github.com-1ecc6299db9ec823/actix-swagger-0.3.0/src/lib.rs:10:26
   |
10 |     http::header::{self, IntoHeaderValue},
   |                          ^^^^^^^^^^^^^^^
   |                          |
   |                          no `IntoHeaderValue` in `http::header`
   |                          help: a similar name exists in the module: `TryIntoHeaderValue`
11 |     http::{HeaderName, HeaderValue},
   |            ^^^^^^^^^^  ^^^^^^^^^^^ no `HeaderValue` in `http`
   |            |
   |            no `HeaderName` in `http`


Generate path with `Answer`

    pub mod session_get {
        use super::responses;
        use actix_swagger::ContentType;
        use actix_web::http::StatusCode;
        use serde::Serialize;

        pub type Answer = actix_swagger::Answer<'static, Response>;

        #[derive(Debug, Serialize)]
        #[serde(untagged)]
        pub enum Response {
            Ok(responses::SessionGetSuccess),
            Unauthorized,
            Unexpected,
        }

        impl Response {
            #[inline]
            pub fn into_answer<'a>(self) -> Answer {
                let status = match self {
                    Self::Ok(_) => StatusCode::OK,
                    Self::Unauthorized => StatusCode::UNAUTHORIZED,
                    Self::Unexpected => StatusCode::INTERNAL_SERVER_ERROR,
                };

                let content_type = match self {
                    Self::Ok(_) => Some(ContentType::Json),
                    Self::Unauthorized => None,
                    Self::Unexpected => None,
                };

                Answer::new(self).status(status).content_type(content_type)
            }
        }
    }

no default features for actix-web

If/when the actix-web dependency is updated to v3, the dependency in Cargo.toml should ideally be:

- actix-web = "3.0.2"
+ actix-web = { version = "3.0.2", default-features = false }

Usage issue

Hi,
I have issue to integrate the library. My code is the following:

Main:

fn main() {
    HttpServer::new(move || {
        App::new()
            .data(AppState {
                pool: pool.clone(),
                log: log.clone(),
            })
            .wrap(middleware::Logger::default())
            .route("/", web::get().to(status))
            .route("/test{_:/?}", web::get().to(test))
    })
    .bind(format!("{}:{}", config.server.host, config.server.port))?
    .run()
    .await
}

Handler:

pub async fn status() -> Result<impl Responder, AppError> {
    Ok(web::HttpResponse::Ok().json(Status {
        status: "Up".to_string(),
    }))
}

pub async fn test(state: web::Data<AppState>) -> Result<impl Responder, AppError> {
    let sublog = state.log.new(o!("handler" => "test"));

    let client: Client = get_client(state.pool.clone(), sublog.clone()).await?;

    let result = db::get_test(&client).await;

    result
        .map(|test| HttpResponse::Ok().json(test))
        .map_err(log_error(sublog))
}

Isolate implementation details from application

Instead of passing body, header, path extractors to a route:

pub async fn route(
    body: web::Json<request_bodies::Register>,
    app: web::Data<crate::App>,
) -> Answer<'static, register_request::Response> {

Generate a single extractor for all data. Router implementation doesn't know about API details:

pub async fn route(
    params: paths::register_request::Params,
    app: web::Data<crate::App>,
) -> Answer<'static, register_request::Response> {

Generator fails on simple input

Openapi.yaml

x-generator: NSwag v13.3.0.0 (NJsonSchema v10.1.11.0 (Newtonsoft.Json v12.0.0.0))
openapi: 3.0.0
info:
  title: Settings API
  description: Settings and presets service
  version: v1
servers:
  - url: 'https://myUrl'
paths:
  /:
    get:
      tags:
        - About
      operationId: About_Get
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageModel'
  /settings/api:
    get:
      tags:
        - About
      operationId: About_Get2
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageModel'
  /Healthcheck:
    get:
      tags:
        - Healthcheck
      operationId: Healthcheck_Get
      responses:
        '200':
          description: ''
  /settings/api/Healthcheck:
    get:
      tags:
        - Healthcheck
      operationId: Healthcheck_Get2
      responses:
        '200':
          description: ''
components:
  schemas:
    MessageModel:
      type: object
      additionalProperties: false
      properties:
        message:
          type: string
          nullable: true
    Integration:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: guid
        createdBy:
          type: string
          format: guid
        createdOn:
          type: string
          format: date-time
        modifiedBy:
          type: string
          format: guid
          nullable: true
        modifiedOn:
          type: string
          format: date-time
          nullable: true
        deletedReason:
          type: string
          nullable: true
        deletedBy:
          type: string
          format: guid
          nullable: true
        deletedOn:
          type: string
          format: date-time
          nullable: true
        companyId:
          type: string
          format: guid
        title:
          type: string
          nullable: true
  securitySchemes:
    Bearer:
      type: apiKey
      description: Please insert Auth token into field
      name: Authorization
      in: header
security:
  - Bearer: []

Command:

cargo-swagg ./openapi.yaml --out-file ./src/api.rs

Expected result:

Got API

Actual result:

#![allow(dead_code, unused_imports)]
pub mod api {
    #[doc = "Settings and presets service"]
    pub struct SettingsApi {
        api: actix_swagger::Api,
    }
    impl SettingsApi {
        pub fn new() -> Self {
            Self {
                api: actix_swagger::Api::new(),
            }
        }
    }
    impl Default for SettingsApi {
        fn default() -> Self {
            let api = Self::new();
            api
        }
    }
    impl actix_web::dev::HttpServiceFactory for SettingsApi {
        fn register(self, config: &mut actix_web::dev::AppService) {
            self.api.register(config);
        }
    }
    use super::paths;
    use actix_swagger::{Answer, Method};
    use actix_web::{dev::Factory, FromRequest};
    use std::future::Future;
    impl SettingsApi {}
}
pub mod components {
    pub mod parameters {
        use serde::{Deserialize, Serialize};
    }
    pub mod request_bodies {
        use serde::{Deserialize, Serialize};
    }
    pub mod responses {
        use serde::{Deserialize, Serialize};
    }
}
pub mod paths {
    use super::components::{parameters, responses};
}

Ignore clippy warnings in generated code

I think it worth adding

#![allow(clippy::all, clippy::restriction, clippy::pedantic, clippy::nursery, clippy::cargo)]

to the existing #![allow(dead_code, unused_imports)] disabled warnings.

Custom attributes

Single attribute:

schema:
  properties:
    demo:
      type: string
        x-rust-attribute: serde(skip_serializing_if = "Option::is_none")

Should generate something like:

struct Example {
  #[serde(skip_serializing_if = "Option::is_none")]
  demo: String,
}

Multiple attributes:

schema:
  properties:
    demo:
      type: string
        x-rust-attribute:
          - serde(skip_serializing_if = “Option::is_none”)
          - validate(url)

Output:

struct Example {
  #[serde(skip_serializing_if = "Option::is_none")]
  #[validate(url)]
  demo: String,
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.