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

Version intermédiaire.

parent 49d6de7c
......@@ -150,10 +150,9 @@ final class ModelGenerator
}
sort($context['repoImports']);
$this->generateFile($writer, 'resources.ts', $context);
$this->generateFile($writer, 'repositories.ts', $context);
$this->generateFile($writer, 'metadata.ts', $context);
$this->generateFile($writer, 'index.ts', $context);
foreach (glob(__DIR__ . '/Resources/views/output/*.ts.twig') as $template) {
$this->generateFile($writer, basename($template, '.twig'), $context);
}
}
/**
......@@ -166,7 +165,7 @@ final class ModelGenerator
$fileWriter = $writer->newFile($path);
try {
$template = sprintf('@NgModelGenerator/%s.twig', basename($path));
$template = sprintf('@NgModelGenerator/output/%s.twig', basename($path));
$context['filename'] = $path;
$fileWriter->write($this->twigEnv->render($template, $context));
} finally {
......
{% extends '@NgModelGenerator/_layout.ts.twig' %}
{% import '@NgModelGenerator/_macros.ts.twig' as m %}
{% block content %}
{% autoescape false %}
import { Injectable, Provider } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
APIMeta,
APIRepositoryRegistry,
AbstractAPIService,
AbstractResourceCache,
IRIMetadata,
LazyMetadataRegistry,
ResourceMetadata,
UUID,
} from '@devatscience/ng-model-runtime';
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.
*/
export interface AppAPI extends APIMeta {
{%- for repo in repositories %}
{{ repo.resourceName | objectKey }}: {
resource: {{ repo.resourceName }};
repository: {{ repo.usage }};
metadata: {{ m.metadataType(repo) }};
iriParameters: {{ m.iriParameterType(repo) }};
};
{% endfor %}
}
/**
* Metadonnées des ressources.
*/
@Injectable()
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 %}this.{{ type.name }}, {% endfor -%}
{%- endset %}
{{ name | objectKey }}: () => new {{ m.metadataType(repo) }}(
{{ name | quoteString }},
new IRIMetadata(
/^{{ repo.iri.testPattern }}$/u,
/^{{ repo.iri.capturePattern }}$/u,
(
{%- if repo.iri.parameters | length > 1 %}[{% endif -%}
{%- for param in repo.iri.parameters -%}
{{ param.name }}{% if not loop.last %}, {% endif -%}
{%- endfor -%}
{%- if repo.iri.parameters | length > 1 %}]{% endif -%}
: {{ 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 %}
),
{%- endfor %}
});
};
{% for repo in repositories %}
public get {{ repo.resourceName }}(): {{ m.metadataType(repo) }} { return this.getOrCreate({{ repo.resourceName | quoteString }}); }
{% endfor %}
}
/**
* Repositories.
*/
@Injectable()
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: AppMetadata,
private readonly cache: AbstractResourceCache,
private readonly client: HttpClient
) {}
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> {
public constructor(
metadata: AppMetadata,
repositories: AppRepositories,
cache: AbstractResourceCache,
client: HttpClient
) {
super(metadata, repositories, cache, client);
}
}
/* Provider factories */
{% for repo in repositories %}
export function {{ repo.usage }}Factory(reps: AppRepositories): {{ repo.usage }} { return reps.{{ repo.resourceName }}; }
{% endfor %}
/**
* Providers Angular
*/
export const PROVIDERS: Provider[] = [
AppMetadata,
AppRepositories,
AppAPIService,
{% for repo in repositories %}
{ provide: {{ repo.usage }}, useFactory: {{ repo.usage }}Factory, deps: [AppRepositories] },
{% endfor %}
];
{% endautoescape -%}
{% endblock content %}
{% extends '@NgModelGenerator/_layout.ts.twig' %}
{% block content %}
export * from '@devatscience/ng-model-runtime';
{% endblock content %}
{% extends '@NgModelGenerator/_layout.ts.twig' %}
{% import '@NgModelGenerator/_macros.ts.twig' as m %}
{% block content %}
{% autoescape false %}
import { Injectable, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
AbstractResourceCache,
Collection,
GetIRIParameters,
GetResourceType,
GetResourceTypeString,
IRI,
IRIMetadata,
RequestOptions,
Resource,
ResourceMetadata,
UUID,
} from '@devatscience/ng-model-runtime';
import { Observable, forkJoin } from 'rxjs';
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';
/**
* Catalogue des repositories.
*/
@Injectable({providedIn: 'root'})
export class AppRepositories {
public constructor(private readonly injector: Injector) {}
{% for repo in repositories %}
public get {{ repo.resourceName }}() { return this.injector.get({{ repo.usage }}); }
{% endfor %}
}
/**
* Facade de l'API.
*/
@Injectable({providedIn: 'root'})
export class AppAPIService {
public constructor(
public readonly repositories: AppRepositories,
private readonly cache: AbstractResourceCache,
private readonly client: HttpClient,
) {
}
public get<R extends AppRepositories[keyof AppRepositories]['get']>>(
iri: IRI<R>,
options?: RequestOptions,
): Observable<R>;
public get<T extends keyof AppMetadata>(
type: T,
parameters: GetIRIParameters<AppMetadata[T]>,
options?: RequestOptions,
): Observable<GetResourceType<AppMetadata[T]>>;
public get<T extends keyof AppMetadata, R extends Resource = GetResourceType<AppMetadata[T]>>(
typeOrIRI: T | IRI<R>,
parametersOrOptions?: GetIRIParameters<AppMetadata[T]> | RequestOptions,
options?: RequestOptions,
): Observable<R> {
let iri: IRI<R>;
if (this.metadata[typeOrIRI as string]) {
iri = this.metadata[typeOrIRI as string]
.generateIRI(parametersOrOptions as GetIRIParameters<AppMetadata[T]>);
} else {
iri = typeOrIRI as IRI<R>;
options = parametersOrOptions as RequestOptions;
}
return this.cache.get(iri, () =>
this.client.get<R>(iri as any, options),
);
}
public getMany<R extends Resource>(
iris: IRI<R>[],
options?: RequestOptions,
): Observable<R[]> {
return forkJoin(iris.map((iri) => this.get(iri, options)));
}
public generateIRI<T extends keyof AppMetadata>(
type: T,
parameters: GetIRIParameters<AppMetadata[T]>,
): IRI<GetResourceType<AppMetadata[T]>> {
return this.metadata[type].generateIRI(parameters);
}
public invalidate<R extends Resource>(iri: IRI<R>): void {
this.cache.invalidate(iri);
}
}
{% endautoescape %}
{% endblock content %}
{% extends '@NgModelGenerator/_layout.ts.twig' %}
{% block content %}
export * from '@devatscience/ng-model-runtime';
export * from './resources';
export * from './repositories';
export * from './compat';
export * from './metadata';
export * from './providers';
export * from './repositories';
export * from './resources';
{% endblock content %}
......@@ -5,16 +5,20 @@
{% block content %}
{% autoescape false %}
import { Observable } from 'rxjs';
import { HttpResponseBase } from '@angular/common/http';
import { Injectable } from "@angular/core";
import {
AbstractRepository,
Collection,
IRI,
IRIMetadata,
RequestOptions,
ResourceMetadata,
UUID,
hasProperty
hasProperty,
Client,
InvalidIRIError
} from '@devatscience/ng-model-runtime';
import { {% for name in repoImports %}
{{ name }}{% if not loop.last %}, {% endif %}
{%- endfor %}
......@@ -24,7 +28,50 @@ import { {% for name in repoImports %}
* Repositories
*********************************************************************************/
{% for repo in repositories %}
export class {{ repo.name }} extends AbstractRepository<{{ repo.resource.usage }}, {{ repo.resourceName | quoteString }}, {{ m.iriParameterType(repo) }}> {
@Injectable({
providedIn: "root",
deps: [Client],
useFactory: (client: Client) => new {{ repo.name }}(client)
})
export class {{ repo.name }} extends AbstractRepository<
{{- repo.resourceName }}, '{{ repo.resourceName }}', {{ m.iriParameterType(repo) -}}
> {
public constructor(protected readonly client: Client) {
super(
'{{ repo.resourceName }}',
[
{% if repo.resource.properties is defined -%}
{%- for name, property in repo.resource.properties -%}
{% if not property.nullable -%}
{{ property.name | quoteString }},
{% endif -%}
{% endfor -%}
{% endif -%}
],
/^{{ repo.iri.capturePattern }}$/u
);
}
public generateIRI(
{%- for param in repo.iri.parameters -%}
{{ param.name }}: {{ param.type.usage }}{% if not loop.last %}, {% endif -%}
{%- endfor -%}
): IRI<{{ repo.resourceName }}> {
return {{ repo.iri.usage }};
}
public getIRIParameters(iri: IRI<{{ repo.name }}>): {{ m.iriParameterType(repo) }} {
const captures = this.iriPattern.exec(iri);
if (!captures) {
throw new InvalidIRIError(`invalid {{ repo.resourceName }} URI: "${iri}"`);
}
{% if repo.iri.parameters | length > 1 -%}
return captures.slice(1);
{%- else %}
return captures[1];
{%- endif %}
}
{% for op in repo.operations %}
{% if op.description -%}
......
import { forkJoin, Observable } from 'rxjs';
import { HttpClient, HttpResponseBase } from '@angular/common/http';
import { IRI, Resource, Collection, RequestOptions } from './types';
import { IRIParameters, ResourceMetadata } from './metadata';
/**
* AbstractResourceCache est une classe abstraite qui définit l'interface d'un cache de ressources.
*
* Elle doit être implémentée puis être fournie en provider d'un module core. Par exemple:
*
* final class ResourceCache<T extends Resource> extends AbstractResourceCache<T> {
* // Implémentation
* }
*
* providers: [
* [ provider: AbstractResourceCache, useClass: ResourceCache ],
* ]
*/
export abstract class AbstractResourceCache {
/**
* Récupère une ressource par son IRI. N'exécute la requête requestFactory que si on ne dispose
* pas d'une version en cache.
*/
public abstract get<R extends Resource>(
iri: IRI<R>,
requestFactory: () => Observable<R>,
): Observable<R>;
/**
* Met à jour une ressource existante, rafraîchit le cache local avec la réponse.
*/
public abstract put<R extends Resource>(
iri: IRI<R>,
request: Observable<R>,
): Observable<R>;
/**
* Crée une nouvelle ressource et met la ressource créée dans le cache.
*/
public abstract post<R extends Resource>(
request: Observable<R>,
): Observable<R>;
/**
* Supprime une ressource en distant et dans le cache.
*/
public abstract delete<R extends Resource>(
iri: IRI<R>,
request: Observable<HttpResponseBase>,
): Observable<HttpResponseBase>;
/**
* Effectue une recherche et met en cache toutes les ressources récupérées.
*/
public abstract getAll<R extends Resource>(
request: Observable<Collection<R>>,
): Observable<Collection<R>>;
/**
* Invalide une ressource en cache.
*/
public abstract invalidate<R extends Resource>(iri: IRI<R>): void;
}
/**
* Classe de base d'un repository.
*/
export abstract class AbstractRepository<
R extends Resource,
T extends string,
P extends IRIParameters
> {
public constructor(
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);
}
/**
* Extrait les paramètres d'une IRI.
*/
public getIRIParameters(iri: IRI<R>): P {
return this.metadata.getIRIParameters(iri);
}
}
/**
* Sur-type encadrant des API.
*/
export interface APIMeta {
[type: string]: {
resource: Resource;
metadata: ResourceMetadata<any, any, IRIParameters>;
repository: AbstractRepository<any, any, IRIParameters>;
iriParameters: IRIParameters;
};
}
/**
* Classe abstraite d'un registre des metadonnées des ressources.
*/
export interface APIMetadataRegistry<API extends APIMeta> {
/**
* Vérifie si on a des métadonnées pour le type de ressourcce indiqué.
*/
has<T extends keyof API>(type: T): type is T;
/**
* (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 has<T extends keyof API>(type: T): type is T {
return typeof type === 'string' && type in this.builders;
}
public get<T extends keyof API>(type: T): API[T]['metadata'] {
if (!this.has(type)) {
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] as API[T]['metadata'];
}
}
/**
* 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 ou par son type et les paramètres de son IRI.
*/
get<R extends Resource>(
iri: IRI<R>,
options?: RequestOptions,
): Observable<R>;
get<T extends keyof API>(
type: T,
parameters: API[T]['iriParameters'],
options?: RequestOptions,
): Observable<API[T]['resource']>;
/**
* Récupère des ressources par leurs IRIs.
*/
getMany<R extends Resource>(
iris: IRI<R>[],
options?: RequestOptions,
): 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
*/
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>,
options?: RequestOptions,
): Observable<R>;
public get<T extends keyof API>(
type: T,
parameters: API[T]['iriParameters'],
options?: RequestOptions,
): Observable<API[T]['resource']>;
public get<T extends keyof API, R extends Resource = API[T]['resource']>(
typeOrIRI: T | IRI<R>,
parametersOrOptions?: API[T]['iriParameters'] | RequestOptions,
options?: RequestOptions,
): Observable<R> {
let iri: IRI<R>;
if (this.metadata.has(typeOrIRI as string)) {
iri = this.metadata
.get(typeOrIRI as string)