swagger-api / swagger-codegen

swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs in different languages by parsing your OpenAPI / Swagger definition.
http://swagger.io
Apache License 2.0
17.01k stars 6.03k forks source link

How to use swagger-codegen for Typescript-angular2 #3221

Closed rajeget closed 7 years ago

rajeget commented 8 years ago

Reference https://github.com/swagger-api/swagger-codegen/tree/master/samples/client/petstore/typescript-angular2/npm

These are ts file , how to compile and create a client ?

Please help

wing328 commented 8 years ago

Would this help? https://github.com/swagger-api/swagger-codegen/blob/master/samples/client/petstore/typescript-angular/README.md (which is for angular1)

rajeget commented 8 years ago

No @wing328 the link is for angular1 as you said , now to make it compile and work for angular2 . We need a package.json (atleast). I used the reference from (https://github.com/swagger-api/swagger-codegen/tree/master/samples/client/petstore/typescript-angular2/npm) and tried making the required file but its seems I need some help here

wing328 commented 8 years ago

cc @Vrolijkx

sebastianhaas commented 8 years ago

@rajegit could you describe your problem a little bit more in detail? I didn't go the npm way but put the generated api client directly into my ng project. Works perfectly fine for me.

rajeget commented 8 years ago

@sebastianhaas . I did following

  1. Generated code for 'typescript-angular2' ( tried both using cli and editor)
  2. I wanted to use the #1 code inside my angular2 project, which is executed using gulp or npm start etc. A standard angular2 project has a index.html , pakage.json etc , its missing in this generated code. So the question is "How did you directly put it into your ng project"
sebastianhaas commented 8 years ago

This codegen doesn't output an angular 2 project, just the required files for a ng2-compatible api client. To use it, you have to include it like any other typescript library using something like:

import { YourModelApi, YourModel } from './api';

If you want, you can check out how I did it for a recent project of mine: https://github.com/sebastianhaas/medical-appointment-scheduling/tree/master/src

It's a 1:1 fork of the angular2 class startup project, and I just pointed the swagger codegen output to src/app/api/. Take a look at, for instance, https://github.com/sebastianhaas/medical-appointment-scheduling/blob/master/src/app/appointment/patient.component.ts to see how to import a specific API endpoint.

wing328 commented 7 years ago

@rajegit if you still need help using the TS angular2 client, please reply to let us know.

dinvlad commented 7 years ago

Not to be nitpicky here, but would it be possible to rename API classes as Services, e.g. PetService, StoreService etc. instead of PetApi, StoreApi etc.? In addition, could we name the corresponding files in lower-dot case, e.g. pet.service.ts, store.service.ts etc., in convention with Angular 2 style guide?

I notice that in @sebastianhaas example, they are already renamed. Was it done by hand or in a script?

sebastianhaas commented 7 years ago

Take a look at the 2.3.0 branch, it contains a PR where I introduced the changes you requested. Btw if you need a module that is suitable for AOT compiling, I did that already, just need to find time to submit a PR.

dinvlad commented 7 years ago

@sebastianhaas, thanks - for some reason 2.3.0 build fails for me. I'll probably just stick to 2.2.2 for now, and when 2.3 comes out I'll do a one-time refactoring for my client.

sebastianhaas commented 7 years ago

Noo, 2.3.0 works fine, I just built it. Make sure you're having Java >= 1.7, and maybe try running mvn clean package -DskipTests=true to skip tests. What's your build's error output? Running a build with tests takes about 8 minutes on my machine, without tests it's just about 30 seconds.

I guess it would be possible to provide a 2.3.0 binary release somewhere, but the one-time-refactoring thing doesn't sound good :)

dinvlad commented 7 years ago

Got it working. I needed to upgrade Maven from 2 to 3, update openjdk-7, and check out 2.3.0 locally. It's build in <2 min (with tests!) and generates the services well! Thanks @sebastianhaas!

dverdin commented 7 years ago

Yay, thanks @sebastianhaas ! Your change to 2.3.0 really made my day! Great work!

fluffybonkers commented 7 years ago

I would love if someone could help explain how to use the Angular 2 client generated by Swagger @wing328

I have it in my project and that is all good, however, I don't get how to setup the Configuration class so that my Authorization headers get set.

I am using a JWT for authentication, but I think it still needs to be put into the ApiKeys property of the Configuration class - is that right?

I have the following component:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { SelectItem, MenuItem } from 'primeng/primeng';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';

import { AuthService } from '../service/authservice'
import { ReportingService } from '../service/generated/api/reporting.service'

@Component({
    templateUrl: './dashboard.component.html',
    providers: [ReportingService]
})
export class DashboardComponent implements OnInit, OnDestroy {

    private todo: any;
    subscription: Subscription;

    constructor(private reporting: ReportingService, private auth: AuthService) { }

    ngOnInit() {
        this.subscription = this.reporting.reportingCountTodo().subscribe(todo => { this.todo = todo; });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}

But I have no idea how to make sure my authorization header gets set when I call reportingCountTodo().

Any help is much appreciated.

dinvlad commented 7 years ago

When you import ApiModule into your AppModule, you can use

@NgModule({
  imports: [
    ApiModule.forConfig({ apiKeys: [...] }),
    ...
  ],
  ...
})

just like using .forRoot(...) in any other module, as explained here: https://angular.io/guide/ngmodule#configure-core-services-with-coremoduleforroot

vasl12 commented 7 years ago

Do I have to do the same if I want to apply basic authentication? How can I provide every time the username and the password?

dinvlad commented 7 years ago

Do I have to do the same if I want to apply basic authentication?

Yes, as far as I can tell:

export interface ConfigurationParameters {
    apiKeys?: {[ key: string ]: string};
    username?: string;
    password?: string;
    accessToken?: string;
    basePath?: string;
}

Also make sure you run v2.3 (build from sources), not sure if all support made it into the stable branch yet.

How can I provide every time the username and the password?

Not sure what you mean - would it be ok to set username and password once when loading the app, or do you have to change them dynamically?

vasl12 commented 7 years ago

As I am new to angular 2, I do not understand what is the ApiModule and how through it I can pass the required parameters for authentication.

Not sure what you mean - would it be ok to set username and password once when loading the app, or do you have to change them dynamically?

No I just need to set them once when loading the app

dinvlad commented 7 years ago

ApiModule is what gets generated by this package (swagger-codegen). So after generation of the code you end up with a folder that contains api.module.ts as well as individual services under api/. Instead of importing these services one by one into your AppModule, you can (should) import the entire api.module.ts as a regular module. This is done only once in the AppModule (and not any other modules, if you have any) because services have to be singletons. Then, to use the service in a component, you just import that service class definition inside that component (not to be confused with importing the service instance, which is done only once in AppModule).

Please take a look at https://angular.io/guide/ngmodule to clarify things further.

vasl12 commented 7 years ago

ApiModule is what gets generated by this package (swagger-codegen). So after generation of the code you end up with a folder that contains api.module.ts as well as individual services under api/. Instead of importing these services one by one into your AppModule, you can (should) import the entire api.module.ts as a regular module. This is done only once in the AppModule (and not any other modules, if you have any) because services have to be singletons.

I used swagger editor for the client generation, so I do not have api.module.ts under api/ directory. I will try again with swagger-codegen.

Please take a look https://angular.io/guide/ngmodule to clarify things further.

thanks a lot

dinvlad commented 7 years ago

Ah, that's why I recommend building Codegen from branch 2.3. It contains proper structures.

dinvlad commented 7 years ago

Correction: in the latest builds on the 2.3 branch, they use a different syntax for forConfig: you now need to provide not the config object itself, but rather a 'config factory' - a function that will return a Configuration object. So, in your app.module.ts:

export function apiConfig() {
  return new Configuration({
    username: '...',
    password: '...',
    basePath: '...',
  });
}
...
@NgModule({
  imports: [
    ApiModule.forConfig(apiConfig),
    ...
  ],
  ...
})
export class AppModule { }

Hope that helps.

vasl12 commented 7 years ago

Thank you for your answers they helped a lot Regarding this

export function apiConfig() { return new Configuration({ username: '...', password: '...', basePath: '...', }); }

Do you know how can I pass arguments from my login service to this function in order to pass them to ApiModule?

dinvlad commented 7 years ago

Do you know how can I pass arguments from my login service to this function in order to pass them to ApiModule?

One solution is to store the credentials in localStorage and then load them from there. A side advantage of that is your auth system may be decoupled from Angular (e.g. you could use a 3rd-party login screen, like those by Auth0 or Stormpath, without "embedding" them in your Angular code). Another advantage is that your login info could be stored reliably across tabs and sessions. That goes with the usual caveat that you need to take precautions against someone stealing that info from the user's browser.

dinvlad commented 7 years ago

FWIW if you're serious about using Angular as a client for your API, I'd recommend using a temporary token (e.g, JWT) instead of permanent credentials for API authentication. That way, even if someone else manages to access those credentials, they will be of limited use.

vasl12 commented 7 years ago

I am just writing an api in order to learn angular, so it is not for serious use yet.

I meant something simpler, just how I can get the credentials from login service and inject them to the service that handles the http calls through this factory function

dinvlad commented 7 years ago

Understood. Then probably passing them through localStorage is the easiest approach.

marcorinck commented 7 years ago

@dinvlad I'm using the current 2.3.0-SNAPSHOT version and the generated typescript-angular2 code looks much better than the current stable 2.2.2. Thanks for the hard work!

The problem though that I have is that I can't change the basePath during runtime of the app because every service is setting it in a local (protected) member variable during construction like this:

constructor(protected http: Http, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) {
        if (basePath) {
            this.basePath = basePath;
        }
        if (configuration) {
            this.configuration = configuration;
            this.basePath = basePath || configuration.basePath || this.basePath;
        }
    }

Because of this the basePath needs to be known before bootstrapping the app and can't be changed any time later. I don't think its good behaviour when every service sets a local copy of the basePath at construction time when there is a configuration object. This leads to inconsistent behaviour too because you can change every configuration parameter on the configuration object but changes to the basePath are silently ignored.

And besides that, I wanted to use the generated code in an ionic project which doesn't have an environment system like in angular cli where the environment variables are changed during build time. This means, that its hard to get configuration parameters for environments before bootstrapping the angular app and I would need to set the basePath during runtime of the app. But this doesn't work without changing the generated code manually after every change in the yaml file.

I don't know if this is the right place to discuss this though. Do you plan to do further changes on the generated code?

sebastianhaas commented 7 years ago

@marcorinck I don't really see why you would want to change the URL an Angular REST service is pointing to at runtime. I also don't quite get your concerns re inconsistent behavior. It's just an offer, you don't have to or shouldn't use both ways to set the base path.

The OpaqueToken/InjectionToken is pretty much best practice I guess. This way, Angular's DI system takes care of providing the proper values at the right time, and you have full control at the time a service is instantiated which imho is the correct way to do it. This way, you don't break Angular's coupling principles and make your services easily testable. Just my two cents.

I wanted to use the generated code in an ionic project which doesn't have an environment system like in angular cli where the environment variables are changed during build time

Are you using some bundling tools like webpack or browserify? They usually offer functionality to do what you want to achieve.

marcorinck commented 7 years ago

@sebastianhaas To set the base path upon bootstrapping of the angular app when services are created I need to have the value already on hand but this isn't the case when you have different environments like dev, qa and prod. In angular-cli you have the environment files which are overwritten on-the-fly during build, so you can import the dev environment in your code but change this with a simple build parameter.

Ionic 2/3 doesn't have an environment system. They use webpack/rollup internally and have a basic dev/prod environment but nothing like angular-cli provides. Most proposals to do this manually, implement some kind of configuration service which then can be injected in other parts of the application. But this can't be use with the ConfigurationFactory currently used in the typescript-angular2 generator, or can it?

I know that I can write my own code and I don't HAVE to use the generated code. I just wanted to give my 2 cents.

But why do you set the basePath on every service locally? Why have a common configuration object where everything can be changed during runtime but (silently) not the basePath?

sebastianhaas commented 7 years ago

They use webpack/rollup internally and have a basic dev/prod environment but nothing like angular-cli provides

https://webpack.js.org/plugins/define-plugin/ is what you want. I've posted a link to the famous Angularclass startup repository as well as a little project of mine where I am actively using this swagger codegen in combination with Webpack and Angular.

I know that I can write my own code and I don't HAVE to use the generated code.

That is not what I wanted to say. I wanted to point out that I don't share your concerns regarding inconsistencies, since you're not supposed to use all the different ways this API client provides for setting a base path in parallel but rather stick with one.

But why do you set the basePath on every service locally? Why have a common configuration object where everything can be changed during runtime but (silently) not the basePath?

I don't. Usually you do this during Angular's bootstrapping, but you can configure Injectors in various other scenarios as well, e.g. for unit or e2e tests. https://angular.io/guide/dependency-injection#configuring-the-injector I don't think your question/problem is related to this codegen though.

marcorinck commented 7 years ago

Maybe I don't make myself clear enough. Lets try again, shall we?

The generator produces this code in api.module.ts:

public static forConfig(configurationFactory: () => Configuration): ModuleWithProviders {
        return {
            ngModule: ApiModule,
            providers: [ {provide: Configuration, useFactory: configurationFactory}]
        }
    }

You call this in the "imports" array of your application module like this:

ApiModule.forConfig(apiConfig),

The configurationFactory could look like this:

function apiConfig() {
    return new Configuration({
        username: '...',
        password: '...',
                basePath: '....',
    });
}

As the generated code doesn't provide any dependencies to the factory provider which is used here I can't use my own configurationService here. I would need to change the api.module.ts to something like this:

 public static forConfig(configurationFactory: () => Configuration): ModuleWithProviders {
        return {
            ngModule: ApiModule,
            providers: [ {provide: Configuration, useFactory: configurationFactory, deps: [MyConfigurationService]}]

        }
    }

The configurationFactory could look like this then:

function apiConfig(myConfigurationService) {
    return new Configuration({
        username: myConfigurationService.username,
        password: myConfigurationService.password,
                basePath: myConfigurationService.basePath,
    });
}

That would indeed solve my problem, but only after a manual change to the generated code.

This doesn't change the problem that I can still inject the Configuration object in my application into other services and components and change ANY configuration during runtime. This would be used instantly but only the change to the basePath would be ignored because that is stored in every generated service locally (outside of the configuration object).

dinvlad commented 7 years ago

Question is, why do you need to change basePath at runtime? Do you serve API from several hosts? If the only reason is to use different values for different environments, then as @sebastianhaas already mentioned, you can (should) use either DefinePlugin or EnvironmentPlugin for Webpack. Then in your code, just use process.env.API_PATH or something similar, and pass the necessary value through a regular environmental variable. The plugins will then automatically substitute process.env.API_PATH with its value at built time.

wing328 commented 7 years ago

Question is, why do you need to change basePath at runtime?

For other clients (e.g ruby, php, C#), we allow changing the basePath during runtime in the configuration object/class. One common use case is for testing in QA, e.g changing basePath from https://api.words.com/v1 to https://api-qa.words.com/v1 or https://localhost:8080/v1 for development.

dinvlad commented 7 years ago

Ok, if basePath changes from one test to another than of course, that helps.

marcorinck commented 7 years ago

I have a problem with the question: "Why do you want to change the basePath?" because it basically means: "Why do you want to use code we generated for you?"

For every line of code there needs to be a reason why it exists and what purpose it serves. So like in a code review I would like to ask this question: Why can I set the basePath in 3 different ways?

  1. the host/basePath value of the spec file (json, yaml) is hard-copied into (each) service as a default
  2. you can create a BASE_PATH InjectionToken. If its set the default value is getting overwritten in every service
  3. if you don't create an InjectionToken you can define a basePath property on the configuration object, which is then copied to every service

I think it would be best if there was only 1 way to configure code.

Maybe the generated code could generate a base config class with some defaults like the host/baseUrl from the spec file which will be used when I don't provide a configuration myself. If I want to change the defaults I would need to create my own instance or create a sub-class which I then can give the .forRoot()/forConfig() method.

The services shouldn't store values of the configuration locally anymore and only use the provided configuration so that developers can decide themselves if a change of value is necessary/sane at any time they want.

sebastianhaas commented 7 years ago

For every line of code there needs to be a reason why it exists and what purpose it serves. So like in a code review I would like to ask this question: Why can I set the basePath in 3 different ways?

Because this is a codegen, so I feel like it is perfectly valid to offer multiple ways to use the generated API client. You don't have to use all of them. But I can see your point, we could think about adding CLI switches so you don't have all three ways compiled in your client.

The services shouldn't store values of the configuration locally anymore and only use the provided configuration

Which basically is what is happening if you're using the InjectionToken.

so that developers can decide themselves if a change of value is necessary/sane at any time they want.

What you want breaks Angular's DI concept. If you need a service with a different configuration go ahead and request a new one with the desired configuration from the DI provider.

Please don't get me wrong here, but I think this is not at all related to swagger codegen but whether you are willing to use Angular in the way it was designed or not :)

https://angular.io/tutorial/toh-pt4 https://angular.io/guide/dependency-injection https://angular.io/guide/lifecycle-hooks https://angular.io/guide/hierarchical-dependency-injection#re-providing-a-service-at-different-levels https://angular.io/guide/hierarchical-dependency-injection#scenario-specialized-providers https://angular.io/guide/testing#test-a-component-with-an-async-service

grEvenX commented 7 years ago

Great work on the 2.3.0 branch, this is going to be really useful.

I have a similar question to that one from @fluffybonkers . Often in an SPA where you need to access an API based on authentication you login first, then get a token (e.g. JWT Token).

Is it somehow possible to change the configuration dynamicly so that after logging in and receving a JWT token, the API services that has been injected to the components gets updated with the authKeys['Authorizaion'] =Bearer `? Perhaps it's my lack of knowledge about the dependency injection in Angular 4, but I don't see how it would work without reloading the page to make sure the component is re-created (since dependency injection is done in the constructor).

sebastianhaas commented 7 years ago

Could you write your token to localStorage? That would allow you to change it on runtime, without reloading and without having to re-create components.

marcorinck commented 7 years ago

@grEvenX

you can inject the configuration objekt anywhere in your application and can change the apiKeys property even during runtime. Like so:

class MyService {
   constructor(private configuration: Configuration, http: Http) {

   }

   login() {
        this.http.post("/login").subscribe((resp) => {
            this.configuration.apiKeys["Authorization"] = response.token;
        })
   }
}

You can change any configuration this way, but not the baseUrl property. I tried to argue on that earlier, but I don't think I got through.

grEvenX commented 7 years ago

@marcorinck Thanks a bunch! That does the trick just fine :) I agree that I don't see why you shouldn't be able to set the baseUrl the same way.

dinvlad commented 7 years ago

@marcorinck looks like that requires modification of the generated code? Or can that be done outside of the ApiModule? Just curious.

marcorinck commented 7 years ago

@dinvlad no, this can be done without changing generated code anywhere anytime in your application. (In the 2.3.0-snapshot branch)

dinvlad commented 7 years ago

Ah, I see - you're passing a statically constructed configuration object through forConfig? Basically,

@NgModule({
  imports: [ ApiModule.forConfig(getApiConfig) ],
})
...
export const apiConfig = new Configuration({
  apiKeys: {},
  ...
});
export function getApiConfig() {
  return apiConfig;
}

and then also injecting apiConfig object into your other services? Seems like a cool idea too.

marcorinck commented 7 years ago

Yes, exactly this

scheler commented 7 years ago

@marcorinck @grEvenX - I dont see the generated code use configuration.apiKeys anywhere; How does the HTTP request send the apiKeys in the headers?

marcorinck commented 7 years ago

@scheler this only works in the 2.3.0-SNAPSHOT branch and - I guess - a correct authorization config in the yaml

securityDefinitions:
  Bearer:
    description: >
      jwt   
    type: apiKey
    name: Authorization
    in: header

And the request definitions need this:

      security:
        - Bearer: []

Then the services will have this auto-generated code::

        // authentication (Bearer) required
        if (this.configuration.apiKeys["Authorization"]) {
            headers.set('Authorization', this.configuration.apiKeys["Authorization"]);
        }
scheler commented 7 years ago

@marcorinck - thank you, I will try this out.

scheler commented 7 years ago

@marcorinck - that worked fine, thanks. Is there a way to configure swagger-codegen to respond to 401 response with a redirect to a login URL? Or I have to add an HTTP interceptor myself?

marcorinck commented 7 years ago

@scheler I don't know of any such possibility and I wouldn't expect that as it is project-specific what to do in this case. You have to write the interceptor yourself (I did the same)