Commit dee6cd84 authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Dernière mouture de l'API Typescript.

parent e10cd35b
......@@ -2,7 +2,8 @@
{% block content %}
import { HttpClient, HttpResponseBase } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
/**
* Nom de la propriété d'une resource contenant son IRI.
......@@ -149,13 +150,17 @@ export class IRIMetadata<P extends string[]> {
public constructor(
private readonly testPattern: RegExp,
private readonly capturePattern: RegExp,
public readonly generate: (parameters: P) => string
private readonly template: (parameters: P) => string
) {}
public validate(path: string): boolean {
return this.testPattern.test(path);
}
public generate(parameters: P): IRI<any> {
return this.template(parameters) as any;
}
public parse(path: string): P {
const matches = this.capturePattern.exec(path);
if (!matches) {
......@@ -178,12 +183,12 @@ function getResourceType(that: any): string | null {
* R : type de resource, e.g. Person
* T : valeur de la propriété '@type', e.g. 'Person'.
*/
export class ResourceMetadata<R extends Resource, T extends string> {
export class ResourceMetadata<R extends Resource, T extends string, P extends string[] = [UUID]> {
public constructor(
public readonly type: T,
private readonly iri: IRIMetadata<any>,
public readonly iri: IRIMetadata<P>,
private readonly requiredProperties: (keyof R)[],
private readonly types: ResourceMetadata<any, any>[] = []
private readonly types: ResourceMetadata<any, any, P>[] = []
) {
this.types.unshift(this);
}
......@@ -205,17 +210,154 @@ export class ResourceMetadata<R extends Resource, T extends string> {
public isRequired(property: keyof R): boolean {
return this.requiredProperties.includes(property);
}
/**
* Génère une IRI à partir de ses paramètres.
*/
public generateIRI(parameters: P): IRI<R> {
return this.iri.generate(parameters);
}
}
/**
* Classe de base d'un repository.
*/
export abstract class AbstractRepository<R extends Resource, T extends string> {
export abstract class AbstractRepository<R extends Resource, T extends string, P extends string[] = [UUID]> {
public constructor(
public readonly metadata: ResourceMetadata<R, T>,
public readonly metadata: ResourceMetadata<R, T, P>,
protected readonly client: HttpClient,
protected readonly cache: AbstractResourceCache
) {}
/**
* Génère une IRI à partir de ses paramètres.
*/
public generateIRI(parameters: P): IRI<R> {
return this.metadata.generateIRI(parameters);
}
}
/**
* Sur-type encadrant des API.
*/
export interface APIMeta {
[type: string]: {
resource: Resource;
metadata: ResourceMetadata<any, any, any>;
repository: AbstractRepository<any, any, any>;
iriParameters: string[];
};
}
/**
* Classe abstraite d'un registre des metadonnées des ressources.
*/
export interface APIMetadataRegistry<API extends APIMeta> {
/**
* (Construit et) retourne l'instance de métadonnées pour le type de resource 'type'.
*/
get<T extends keyof API>(type: T): API[T]['metadata'];
}
/**
* Registre de métadonnées qui construit les instances à la demande (ce qui permet de gérer les
* dépendances entre métadonnées).
*/
export class LazyMetadataRegistry<API extends APIMeta> implements APIMetadataRegistry<API> {
private readonly instances: { [T in keyof API]?: API[T]['metadata'] } = {};
protected constructor(
private readonly builders: { readonly [T in keyof API]: (r?: APIMetadataRegistry<API>) => API[T]['metadata'] }
) {}
public get<T extends keyof API>(type: T): API[T]['metadata'] {
if (typeof type !== 'string' || !(type in this.builders)) {
throw new Error(`Invalid resource type: ${type}`);
}
return this.getOrCreate(type);
}
protected getOrCreate<T extends keyof API>(type: T): API[T]['metadata'] {
if (!(type in this.instances)) {
this.instances[type] = this.builders[type](this);
}
return this.instances[type];
}
}
/**
* Classe abstraite de la façade de l'API.
*/
export interface APIRepositoryRegistry<API extends APIMeta> {
get<T extends keyof API>(type: T): API[T]['repository'];
}
/**
* Service permettant d'accéder à l'API.
*/
export interface APIService<API extends APIMeta> {
/**
* Métadonnées de l'API.
*/
readonly metadata: APIMetadataRegistry<API>;
/**
* Repositories de l'API
*/
readonly repositories: APIRepositoryRegistry<API>;
/**
* Récupère une ressource par son IRI.
*/
get<R extends Resource>(iri: IRI<R>): Observable<R>;
/**
* Récupère des ressources par leurs IRIs.
*/
getMany<R extends Resource>(iris: IRI<R>[]): Observable<R[]>;
/**
* Génère l'IRI d'une resource à partir de son type et des paramètres d'IRI.
*/
generateIRI<T extends keyof API, P extends string[]>(type: T, parameters: P): IRI<any>;
/**
* Invalide le cache pour une IRI.
*/
invalidate<R extends Resource>(iri: IRI<R>): void;
}
/**
* Implémentation de base d'une api
*/
@Injectable()
export abstract class AbstractAPIService<
API extends APIMeta,
MR extends APIMetadataRegistry<API>,
RR extends APIRepositoryRegistry<API>
> implements APIService<API> {
public constructor(
public readonly metadata: MR,
public readonly repositories: RR,
private readonly cache: AbstractResourceCache,
private readonly client: HttpClient
) {}
public get<R extends Resource>(iri: IRI<R>): Observable<R> {
return this.cache.get(iri, () => this.client.get<R>(iri as any));
}
public getMany<R extends Resource>(iris: IRI<R>[]): Observable<R[]> {
return forkJoin(iris.map(iri => this.get(iri)));
}
public generateIRI<T extends keyof API>(type: T, parameters: API[T]['iriParameters']): IRI<API[T]['resource']> {
return this.metadata.get(type).generateIRI(parameters);
}
public invalidate<R extends Resource>(iri: IRI<R>): void {
this.cache.invalidate(iri);
}
}
{% endblock content %}
{% endblock %}
{% extends '@NgModelGenerator/_layout.ts.twig' %}
{% macro iriParameterType(repo) -%}
{%- autoescape false -%}
[ {% for param in repo.iri.parameters -%}
{{- param.type.usage }}{% if not loop.last %}, {% endif -%}
{%- endfor %} ]
{%- endautoescape -%}
{%- endmacro %}
{% macro metadataType(repo) -%}
{%- autoescape false -%}
{%- import _self as m -%}
ResourceMetadata<{{ repo.resourceName }}, {{ repo.resourceName | quoteString }}, {{ m.iriParameterType(repo) }}>
{%- endautoescape -%}
{%- endmacro %}
{% import _self as m %}
{% block content %}
{% autoescape false %}
import { Injectable, Provider } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { IRI, AbstractResourceCache, IRIMetadata, ResourceMetadata, UUID, IRI_PROPERTY, TYPE_PROPERTY, Resource } from './common';
import {
{% for repo in repositories %}
{{ repo.resourceName }}{% if not loop.last %},{% endif %}
{%- endfor %}
} from './resources';
AbstractAPIService,
AbstractResourceCache,
APIMeta,
APIRepositoryRegistry,
IRIMetadata,
LazyMetadataRegistry,
ResourceMetadata,
UUID,
} from './common';
import {
{% for repo in repositories %}
{{ repo.usage }}{% if not loop.last %},{% endif %}
{%- endfor %}
} from './repositories';
import {
{% for repo in repositories %}
{{ repo.resourceName }}{% if not loop.last %},{% endif %}
{%- endfor %}
} from './resources';
/**
* Interface permettant de faire le lien entre le "@type", la ressource, les metadonnées et son repository.
*/
interface Types {
{%- for repo in repositories -%}
interface AppAPI extends APIMeta {
{%- for repo in repositories %}
{{ repo.resourceName | objectKey }}: {
resource: {{ repo.resourceName }};
repository: {{ repo.usage }};
metadata: ResourceMetadata<{{ repo.resourceName }}, {{ repo.resourceName | quoteString }}>;
metadata: {{ m.metadataType(repo) }};
iriParameters: {{ m.iriParameterType(repo) }};
};
{% endfor %}
}
/**
* Ensemble des valeurs valides de @type.
*/
type ResourceTypes = keyof Types;
/**
* Alias pour faciliter le typage MetadataRegistry.
*/
type MetadataRegistryType = { [T in ResourceTypes]?: Types[T]['metadata'] };
/**
* Metadonnées des ressources.
*/
@Injectable()
export class MetadataRegistry {
private readonly instances: Partial<MetadataRegistryType> = {};
private readonly builders: {
readonly [T in ResourceTypes]: (r?: MetadataRegistryType) => MetadataRegistryType[T]
} = {
{%- for repo in repositories -%}
export class AppMetadata extends LazyMetadataRegistry<AppAPI> {
public constructor() {
super({
{%- for repo in repositories %}
{%- set name = repo.resourceName -%}
{%- set subTypes -%}
{%- for type in repo.atTypes if type.name != name %}r.{{ type.name }}, {% endfor -%}
{%- endset -%}
{{ name | objectKey }}: {{ subTypes ? 'r' : '()' }} => new ResourceMetadata<{{ name }}, {{ name | quoteString }}>(
{%- for type in repo.atTypes if type.name != name %}this.{{ type.name }}, {% endfor -%}
{%- endset %}
{{ name | objectKey }}: () => new {{ m.metadataType(repo) }}(
{{ name | quoteString }},
new IRIMetadata(
/^{{ repo.iri.testPattern }}$/u,
......@@ -67,86 +79,67 @@ export class MetadataRegistry {
{%- for param in repo.iri.parameters -%}
{{ param.name }}{% if not loop.last %}, {% endif -%}
{%- endfor -%}
]: [
{%- for param in repo.iri.parameters -%}
{{ param.type.usage }}{% if not loop.last %}, {% endif -%}
{%- endfor -%}
]) => {{ repo.iri.usage }}
]: {{ m.iriParameterType(repo) }}) => {{ repo.iri.usage }}
),
[ {% if repo.resource.properties is defined -%}
{%- for name, property in repo.resource.properties if not property.nullable %}{{ property.name | quoteString }}, {% endfor -%}
{%- endif %}],
{% if subTypes %}[ {{ subTypes }} ]{% endif %}
{%- if subTypes %}
[ {{ subTypes }} ]
{%- endif %}
),
{%- endfor %}
};
});
};
{% for repo in repositories %}
public get {{ repo.resourceName }}(): MetadataRegistryType[{{ repo.resourceName | quoteString }}] { return this.getOrCreate({{ repo.resourceName | quoteString }}); }
public get {{ repo.resourceName }}(): {{ m.metadataType(repo) }} { return this.getOrCreate({{ repo.resourceName | quoteString }}); }
{% endfor %}
public get<T extends ResourceTypes>(type: T): MetadataRegistryType[T] {
if (typeof type !== 'string' || !(type in this.builders)) {
throw new Error(`Invalid resource type: ${type}`);
}
return this.getOrCreate(type);
}
private getOrCreate<T extends ResourceTypes>(type: T): MetadataRegistryType[T] {
if (!(type in this.instances)) {
this.instances[type] = this.builders[type](this);
}
return this.instances[type];
}
}
/**
* Façade de l'API.
* Repositories.
*/
@Injectable()
export class API {
{% for repo in repositories %}
public readonly {{ repo.resourceName }} = new {{ repo.usage }}(this.metadata.{{ repo.resourceName }}, this.client, this.cache);
{% endfor %}
export class AppRepositories implements APIRepositoryRegistry<AppAPI> {
{% for repo in repositories %}
public readonly {{ repo.resourceName }} = new {{ repo.name }}(this.metadata.{{ repo.resourceName }}, this.client, this.cache);
{% endfor %}
public constructor(
public readonly metadata: MetadataRegistry,
public readonly metadata: AppMetadata,
private readonly cache: AbstractResourceCache,
private readonly client: HttpClient
) {}
public getMetadata<T extends ResourceTypes>(type: T): Types[T]['metadata'] {
return this.metadata.get(type);
}
public getRepository<T extends ResourceTypes>(type: T): Types[T]['repository'] {
return this[type];
}
public get<R extends Resource>(iri: IRI<R>): Observable<R> {
return this.cache.get(iri, () => this.client.get<R>(iri as any));
}
public invalidate<R extends Resource>(iri: IRI<R>): void {
this.cache.invalidate(iri);
public get<T extends keyof AppAPI>(type: T): AppAPI[T]['repository'] {
return (this as any)[type];
}
}
/**
* Le service d'API
*/
@Injectable()
export class AppAPIService extends AbstractAPIService<AppAPI, AppMetadata, AppRepositories> {}
/* Provider factories */
{% for repo in repositories %}
export function {{ repo.usage }}Factory(api: API): {{ repo.usage }} { return api.{{ repo.resourceName }}; }
export function {{ repo.usage }}Factory(reps: AppRepositories): {{ repo.usage }} { return reps.{{ repo.resourceName }}; }
{% endfor %}
/**
* Providers Angular
*/
export const PROVIDERS: Provider[] = [
MetadataRegistry,
API,
AppMetadata,
AppRepositories,
AppAPIService,
{% for repo in repositories %}
{ provide: {{ repo.usage }}, useFactory: {{ repo.usage }}Factory, deps: [API] },
{ provide: {{ repo.usage }}, useFactory: {{ repo.usage }}Factory, deps: [AppRepositories] },
{% endfor %}
];
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment