# @metaengine/openapi-angular

Generate TypeScript/Angular services and models from OpenAPI specifications.

## Features

- ✅ **Angular 14+** - Modern Angular with RxJS observables
- ✅ **httpResource (Angular 19.2+)** - Signal-based data fetching with automatic request management
- ✅ **TypeScript** - Fully typed API clients and models
- ✅ **Dependency Injection** - Injectable services with configurable scope
- ✅ **HttpClient Integration** - Native Angular HTTP client
- ✅ **Error Handling** - Smart error handling with interceptors
- ✅ **Modern inject()** - Support for functional injection
- ✅ **Tree-shakeable** - Optimized bundle size with separate files

## Installation

```bash
npm install --save-dev @metaengine/openapi-angular
```

Or use directly with npx:

```bash
npx @metaengine/openapi-angular <input> <output>
```

## Requirements

- Node.js 14.0 or later
- .NET 8.0 or later runtime ([Download](https://dotnet.microsoft.com/download))
- Angular 14.0 or later (for generated code)

## Usage

### With npm scripts (Recommended when installed locally)

If you installed the package with `npm install`, add a script to your `package.json`:

```json
{
  "scripts": {
    "generate:api": "metaengine-openapi-angular api.yaml ./src/app/api --inject-function --error-handling --documentation"
  }
}
```

Then run:
```bash
npm run generate:api
```

### With npx (One-off usage or without installation)

Use `npx` for trying out the tool or in CI/CD pipelines:

```bash
npx @metaengine/openapi-angular <input> <output> [options]
```

### Quick Examples

```bash
# Recommended (follows Angular style guide)
npx @metaengine/openapi-angular api.yaml ./src/app/api \
  --inject-function \
  --error-handling \
  --documentation

# Basic (minimal setup)
npx @metaengine/openapi-angular api.yaml ./src/app/api

# From URL
npx @metaengine/openapi-angular https://api.example.com/openapi.json ./src/app/api \
  --inject-function \
  --documentation

# Filter by tags (only specific endpoints)
npx @metaengine/openapi-angular api.yaml ./src/app/api \
  --include-tags users,auth \
  --inject-function

# Advanced (custom DI scope with strict validation)
npx @metaengine/openapi-angular api.yaml ./src/app/api \
  --inject-function \
  --provided-in any \
  --strict-validation \
  --documentation
```

**Note:** When the package is installed locally, `npx` will use that version instead of downloading a new one.

### CLI Options

| Option | Description | Default |
|--------|-------------|---------|
| `--include-tags <tags>` | Filter by OpenAPI tags (comma-separated, case-insensitive) | - |
| `--provided-in <value>` | Angular injection scope (root, any, platform) | - |
| `--base-url-token <name>` | Injection token name for base URL | `BASE_URL` |
| `--options-threshold <n>` | Parameter count for options object | `4` |
| `--documentation` | Generate JSDoc comments | `false` |
| `--inject-function` | Use inject() instead of constructor injection | `false` |
| `--http-resource` | Use httpResource with Signals for HTTP calls | `false` |
| `--lazy-resource` | Add trigger signal for lazy loading (implies `--http-resource`) | `false` |
| `--error-handling` | Enable smart error handling | `false` |
| `--strict-validation` | Enable strict OpenAPI validation | `false` |
| `--date-transformation` | Convert date strings in responses to Date objects | `false` |
| `--interceptors` | Generate Angular interceptors for cross-cutting concerns | `false` |
| `--clean` | Clean output directory (remove files not in generation) | `false` |
| `--verbose` | Enable verbose logging | `false` |
| `--type-mapping <slug=target>` | Override TS type for an OpenAPI format. Repeatable. See [Type mapping overrides](#type-mapping-overrides) | - |
| `--help, -h` | Show help message | - |

### Type mapping overrides

Use `--type-mapping` to override the TS type emitted for a given OpenAPI `(type, format)` pair. Repeatable. Unknown slugs and unknown targets are hard errors.

| Slug | OpenAPI `(type, format)` | Default | `--type-mapping` value |
|------|--------------------------|---------|------------------------|
| `int64` | `(integer, int64)` | `number` | `int64=bigint` |
| `decimal` | `(number, decimal)` | `number` | `decimal=string` |
| `date-time` | `(string, date-time)` | `Date` | `date-time=string` |
| `date` | `(string, date)` | `Date` | `date=string` |

```bash
npx @metaengine/openapi-angular api.yaml ./src/app/api \
  --type-mapping int64=bigint \
  --type-mapping date-time=string
```

## Generated Code Structure

The generator creates a clean, modular structure with separate files for each model and service, optimized for tree-shaking:

```
output/
  ├── models/                    # One file per model
  │   ├── user.ts               # export interface User { ... }
  │   ├── product.ts            # export interface Product { ... }
  │   ├── order.ts              # export interface Order { ... }
  │   └── ...
  ├── services/                  # One file per service/tag
  │   ├── users.service.ts      # UsersService with all user operations
  │   ├── products.service.ts   # ProductsService with all product operations
  │   ├── orders.service.ts     # OrdersService with all order operations
  │   ├── base-api.service.ts   # Base service with common functionality
  │   └── ...
  ├── alias-types.ts            # Type aliases from OpenAPI spec
  ├── dictionary-types.ts       # Dictionary/map types
  └── union-types.ts            # Union types for complex schemas
```

## Example Generated Code

### Model (models/user.ts)
```typescript
export interface User {
    id: string;
    email: string;
    name: string;
    created_at: string;
    roles?: string[];
}
```

### Service (services/users.service.ts)
```typescript
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { BaseApiService } from './base-api.service';
import { User } from '../models/user';
import { CreateUserRequest } from '../models/create-user-request';
import { UsersListResponse } from '../models/users-list-response';

@Injectable({ providedIn: 'root' })
export class UsersService extends BaseApiService {

  /**
   * Get list of users
   * @param {number} limit Maximum number of users to return
   * @param {number} offset Offset for pagination
   * @returns {Observable<UsersListResponse>} Observable containing the users list
   */
  listUsers(limit?: number, offset?: number): Observable<UsersListResponse> {
    const params: Record<string, any> = {};
    if (limit !== undefined) params['limit'] = limit;
    if (offset !== undefined) params['offset'] = offset;

    return this.createRequest(() => this.http.get<UsersListResponse>(
      this.buildUrl('/users'),
      { params: this.createParams(params) }
    ), 'listUsers');
  }

  /**
   * Get a user by ID
   * @param {string} userId User ID
   * @returns {Observable<User>} Observable containing the user
   */
  getUser(userId: string): Observable<User> {
    return this.createRequest(() => this.http.get<User>(
      this.buildUrl(`/users/${userId}`)
    ), 'getUser');
  }

  /**
   * Create a new user
   * @param {CreateUserRequest} request User creation request
   * @returns {Observable<User>} Observable containing the created user
   */
  createUser(request: CreateUserRequest): Observable<User> {
    return this.createRequest(() => this.http.post<User>(
      this.buildUrl('/users'),
      request
    ), 'createUser');
  }
}
```

## httpResource Support (Angular 19.2+)

When using `--http-resource`, the generator creates Signal-based resource methods alongside traditional Observable methods for GET operations:

```typescript
import { Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs';
import { httpResource, HttpResourceRef } from '@angular/common/http';
import { BaseApiService } from './base-api.service';
import { User } from '../models/user';

@Injectable({ providedIn: 'root' })
export class UsersService extends BaseApiService {

  // Traditional Observable method (always available)
  getUser(userId: string): Observable<User> {
    return this.createRequest(() => this.http.get<User>(
      this.buildUrl(`/users/${userId}`)
    ), 'getUser');
  }

  // Signal-based httpResource method (with --http-resource flag)
  getUserResource(userId: Signal<string | undefined>): HttpResourceRef<User | undefined> {
    return httpResource<User>(
      () => {
        const userIdValue = userId();
        if (typeof userIdValue === 'undefined') {
          return undefined;
        }
        return {
          url: this.buildUrl(`/users/${userIdValue}`)
        };
      });
  }
}
```

**Benefits of httpResource:**
- Automatic request lifecycle management
- Reactive updates when Signal inputs change
- Prevents requests when required parameters are undefined
- Perfect for create/edit forms with initially undefined inputs
- Smaller bundle size compared to RxJS for simple GET requests

**Usage in components:**
```typescript
export class UserDetailsComponent {
  private userIdSignal = signal<string | undefined>(undefined);

  // Automatically refetches when userIdSignal changes
  userResource = inject(UsersService).getUserResource(this.userIdSignal);

  loadUser(id: string) {
    this.userIdSignal.set(id);  // Triggers automatic refetch
  }
}
```

### Lazy Loading with `--lazy-resource`

By default, parameterless GET endpoints fire immediately because there are no signal dependencies.
With `--lazy-resource`, all httpResource methods get an optional `trigger` signal parameter that
controls when the resource loads:

```bash
npx @metaengine/openapi-angular api.yaml ./src/app/api --lazy-resource
```

```typescript
// Parameterless endpoint — lazy with trigger signal
getAccountsResource(trigger?: Signal<unknown>): HttpResourceRef<Account[]> {
  return httpResource<Account[]>(
    () => {
      if (trigger && trigger() === undefined) {
        return undefined;
      }
      return {
        url: this.buildUrl(`/api/accounts`)
      };
    },
    { defaultValue: [] });
}

// Parameterized endpoint — trigger appended after existing params
getUserByIdResource(id: Signal<number | undefined>, trigger?: Signal<unknown>): HttpResourceRef<User | undefined> {
  return httpResource<User>(
    () => {
      const idValue = id();

      if (trigger && trigger() === undefined) {
        return undefined;
      }

      if (typeof idValue === 'undefined') {
        return undefined;
      }
      return {
        url: this.buildUrl(`/api/users/${idValue}`)
      };
    });
}
```

**Usage — lazy load on demand (e.g., multi-tenant app):**
```typescript
export class AccountsComponent {
  private loadTrigger = signal<unknown>(undefined);

  // Resource stays idle until trigger fires
  accountsResource = inject(AccountsService).getAccountsResource(this.loadTrigger);

  onCompanySelected() {
    this.loadTrigger.set(true);  // Triggers the HTTP request
  }
}
```

Without the trigger parameter (or when omitted), methods behave exactly as before.

## License

MIT

## Support

For issues and feature requests, please visit:
https://github.com/meta-engine/openapi-angular/issues