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

Dernière mouture de l'API Typescript.

Showing with 221 additions and 86 deletions
+221 -86
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
{% block content %} {% block content %}
import { HttpClient, HttpResponseBase } from '@angular/common/http'; 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. * Nom de la propriété d'une resource contenant son IRI.
...@@ -149,13 +150,17 @@ export class IRIMetadata<P extends string[]> { ...@@ -149,13 +150,17 @@ export class IRIMetadata<P extends string[]> {
public constructor( public constructor(
private readonly testPattern: RegExp, private readonly testPattern: RegExp,
private readonly capturePattern: RegExp, private readonly capturePattern: RegExp,
public readonly generate: (parameters: P) => string private readonly template: (parameters: P) => string
) {} ) {}
public validate(path: string): boolean { public validate(path: string): boolean {
return this.testPattern.test(path); return this.testPattern.test(path);
} }
public generate(parameters: P): IRI<any> {
return this.template(parameters) as any;
}
public parse(path: string): P { public parse(path: string): P {
const matches = this.capturePattern.exec(path); const matches = this.capturePattern.exec(path);
if (!matches) { if (!matches) {
...@@ -178,12 +183,12 @@ function getResourceType(that: any): string | null { ...@@ -178,12 +183,12 @@ function getResourceType(that: any): string | null {
* R : type de resource, e.g. Person * R : type de resource, e.g. Person
* T : valeur de la propriété '@type', 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 constructor(
public readonly type: T, public readonly type: T,
private readonly iri: IRIMetadata<any>, public readonly iri: IRIMetadata<P>,
private readonly requiredProperties: (keyof R)[], private readonly requiredProperties: (keyof R)[],
private readonly types: ResourceMetadata<any, any>[] = [] private readonly types: ResourceMetadata<any, any, P>[] = []
) { ) {
this.types.unshift(this); this.types.unshift(this);
} }
...@@ -205,17 +210,154 @@ export class ResourceMetadata<R extends Resource, T extends string> { ...@@ -205,17 +210,154 @@ export class ResourceMetadata<R extends Resource, T extends string> {
public isRequired(property: keyof R): boolean { public isRequired(property: keyof R): boolean {
return this.requiredProperties.includes(property); 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. * 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 constructor(
public readonly metadata: ResourceMetadata<R, T>, public readonly metadata: ResourceMetadata<R, T, P>,
protected readonly client: HttpClient, protected readonly client: HttpClient,
protected readonly cache: AbstractResourceCache 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' %} {% 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 %} {% block content %}
{% autoescape false %} {% autoescape false %}
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { IRI, AbstractResourceCache, IRIMetadata, ResourceMetadata, UUID, IRI_PROPERTY, TYPE_PROPERTY, Resource } from './common';
import { import {
{% for repo in repositories %} AbstractAPIService,
{{ repo.resourceName }}{% if not loop.last %},{% endif %} AbstractResourceCache,
{%- endfor %} APIMeta,
} from './resources'; APIRepositoryRegistry,
IRIMetadata,
LazyMetadataRegistry,
ResourceMetadata,
UUID,
} from './common';
import { import {
{% for repo in repositories %} {% for repo in repositories %}
{{ repo.usage }}{% if not loop.last %},{% endif %} {{ repo.usage }}{% if not loop.last %},{% endif %}
{%- endfor %} {%- endfor %}
} from './repositories'; } 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 permettant de faire le lien entre le "@type", la ressource, les metadonnées et son repository.
*/ */
interface Types { interface AppAPI extends APIMeta {
{%- for repo in repositories -%} {%- for repo in repositories %}
{{ repo.resourceName | objectKey }}: { {{ repo.resourceName | objectKey }}: {
resource: {{ repo.resourceName }}; resource: {{ repo.resourceName }};
repository: {{ repo.usage }}; repository: {{ repo.usage }};
metadata: ResourceMetadata<{{ repo.resourceName }}, {{ repo.resourceName | quoteString }}>; metadata: {{ m.metadataType(repo) }};
iriParameters: {{ m.iriParameterType(repo) }};
}; };
{% endfor %} {% 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. * Metadonnées des ressources.
*/ */
@Injectable() @Injectable()
export class MetadataRegistry { export class AppMetadata extends LazyMetadataRegistry<AppAPI> {
public constructor() {
private readonly instances: Partial<MetadataRegistryType> = {}; super({
{%- for repo in repositories %}
private readonly builders: {
readonly [T in ResourceTypes]: (r?: MetadataRegistryType) => MetadataRegistryType[T]
} = {
{%- for repo in repositories -%}
{%- set name = repo.resourceName -%} {%- set name = repo.resourceName -%}
{%- set subTypes -%} {%- set subTypes -%}
{%- for type in repo.atTypes if type.name != name %}r.{{ type.name }}, {% endfor -%} {%- for type in repo.atTypes if type.name != name %}this.{{ type.name }}, {% endfor -%}
{%- endset -%} {%- endset %}
{{ name | objectKey }}: {{ subTypes ? 'r' : '()' }} => new ResourceMetadata<{{ name }}, {{ name | quoteString }}>(
{{ name | objectKey }}: () => new {{ m.metadataType(repo) }}(
{{ name | quoteString }}, {{ name | quoteString }},
new IRIMetadata( new IRIMetadata(
/^{{ repo.iri.testPattern }}$/u, /^{{ repo.iri.testPattern }}$/u,
...@@ -67,86 +79,67 @@ export class MetadataRegistry { ...@@ -67,86 +79,67 @@ export class MetadataRegistry {
{%- for param in repo.iri.parameters -%} {%- for param in repo.iri.parameters -%}
{{ param.name }}{% if not loop.last %}, {% endif -%} {{ param.name }}{% if not loop.last %}, {% endif -%}
{%- endfor -%} {%- endfor -%}
]: [ ]: {{ m.iriParameterType(repo) }}) => {{ repo.iri.usage }}
{%- for param in repo.iri.parameters -%}
{{ param.type.usage }}{% if not loop.last %}, {% endif -%}
{%- endfor -%}
]) => {{ repo.iri.usage }}
), ),
[ {% if repo.resource.properties is defined -%} [ {% if repo.resource.properties is defined -%}
{%- for name, property in repo.resource.properties if not property.nullable %}{{ property.name | quoteString }}, {% endfor -%} {%- for name, property in repo.resource.properties if not property.nullable %}{{ property.name | quoteString }}, {% endfor -%}
{%- endif %}], {%- endif %}],
{% if subTypes %}[ {{ subTypes }} ]{% endif %} {%- if subTypes %}
[ {{ subTypes }} ]
{%- endif %}
), ),
{%- endfor %} {%- endfor %}
}; });
};
{% for repo in repositories %} {% 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 %} {% 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() @Injectable()
export class API { export class AppRepositories implements APIRepositoryRegistry<AppAPI> {
{% for repo in repositories %}
{% for repo in repositories %} public readonly {{ repo.resourceName }} = new {{ repo.name }}(this.metadata.{{ repo.resourceName }}, this.client, this.cache);
public readonly {{ repo.resourceName }} = new {{ repo.usage }}(this.metadata.{{ repo.resourceName }}, this.client, this.cache); {% endfor %}
{% endfor %}
public constructor( public constructor(
public readonly metadata: MetadataRegistry, public readonly metadata: AppMetadata,
private readonly cache: AbstractResourceCache, private readonly cache: AbstractResourceCache,
private readonly client: HttpClient private readonly client: HttpClient
) {} ) {}
public getMetadata<T extends ResourceTypes>(type: T): Types[T]['metadata'] { public get<T extends keyof AppAPI>(type: T): AppAPI[T]['repository'] {
return this.metadata.get(type); return (this as any)[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);
} }
} }
/**
* Le service d'API
*/
@Injectable()
export class AppAPIService extends AbstractAPIService<AppAPI, AppMetadata, AppRepositories> {}
/* Provider factories */ /* Provider factories */
{% for repo in repositories %} {% 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 %} {% endfor %}
/** /**
* Providers Angular * Providers Angular
*/ */
export const PROVIDERS: Provider[] = [ export const PROVIDERS: Provider[] = [
MetadataRegistry, AppMetadata,
API, AppRepositories,
AppAPIService,
{% for repo in repositories %} {% for repo in repositories %}
{ provide: {{ repo.usage }}, useFactory: {{ repo.usage }}Factory, deps: [API] }, { provide: {{ repo.usage }}, useFactory: {{ repo.usage }}Factory, deps: [AppRepositories] },
{% endfor %} {% endfor %}
]; ];
......
Supports Markdown
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