Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Pôle IS
Bundles Symfony 2
ng-model-generator-bundle
Commits
2afefafc
Commit
2afefafc
authored
Feb 16, 2021
by
Guillaume Perréal
Browse files
Débute l'écriture du module TS.
parent
f742e178
Changes
20
Hide whitespace changes
Inline
Side-by-side
src/php/ModelGenerator.php
View file @
2afefafc
...
...
@@ -149,7 +149,6 @@ final class ModelGenerator
}
sort
(
$context
[
'repoImports'
]);
$this
->
generateFile
(
$writer
,
'common.ts'
,
$context
);
$this
->
generateFile
(
$writer
,
'resources.ts'
,
$context
);
$this
->
generateFile
(
$writer
,
'repositories.ts'
,
$context
);
$this
->
generateFile
(
$writer
,
'metadata.ts'
,
$context
);
...
...
src/php/Resources/views/common.ts.twig
deleted
100644 → 0
View file @
f742e178
{%
extends
'@NgModelGenerator/_layout.ts.twig'
%}
{%
block
content
%}
import {HttpClient, HttpHeaders, HttpResponseBase} from '@angular/common/http';
import { forkJoin, Observable } from 'rxjs';
/**
* Nom de la propriété d'une resource contenant son IRI.
*/
export const IRI_PROPERTY = '@id';
/**
* Nom de la propriété d'une resource contenant son type.
*/
export const TYPE_PROPERTY = '@type';
/**
* Nom de la propriété contenant les resource d'une collection.
*/
export const COLLECTION_MEMBERS = 'hydra:member';
/**
* Nom de la propriété contenant le nombre total d'objet d'une collection.
*/
export const COLLECTION_TOTAL_COUNT = 'hydra:totalItems';
/**
* IRI typé.
*
* Internationalized Resource Identifier - RFC 3987
*/
const IRI = Symbol('IRI');
/* Les IRI sont en fait des chaînes mais pour forcer un typage fort on les définit
* comme un type "opaque". De cette façon, il est impossible de mélanger
* IRI et chaînes, et le type générique R permet d'interdire les assignations entre
* IRI de resources différentes.
*/
export interface IRI
<R
extends
Resource
>
{
readonly [IRI]?: R;
}
/**
* Resource
*/
export interface Resource {
readonly [IRI_PROPERTY]: IRI
<any>
;
readonly [TYPE_PROPERTY]: string;
[property: string]: any;
};
/**
* Collection représente une collection de respoucres JSON-LD pour un type T donné.
*/
export interface Collection
<R
extends
Resource
>
{
[COLLECTION_MEMBERS]: R[];
[COLLECTION_TOTAL_COUNT]: number;
[property: string]: any;
}
/**
* Retourne les membres d'une collection.
*/
export function getCollectionMembers
<R
extends
Resource
>
(collection: Collection
<R>
): R[] {
return collection[COLLECTION_MEMBERS];
}
/**
* Retourne le nombre total d'items
* @param collection
*/
export function getCollectionTotalCount
<R
extends
Resource
>
(collection: Collection
<R>
): number {
return collection[COLLECTION_TOTAL_COUNT];
}
/**
* Universally Unique IDentifier - RFC 4122
*/
export type UUID = string;
/**
* Teste si une donnée (chaîne) est formatée selon un UUID.
*/
export function isUUID(data: any): data is UUID {
return typeof data === 'string'
&&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(data);
}
/**
* Full DateTime in ISO-8601 format.
*/
export type DateTime = string;
/**
* Options supplémentaires pour les requêtes.
*/
export interface RequestOptions {
body?: any;
headers?: HttpHeaders | {
[header: string]: string | string[];
};
params?: { [param: string]: string | string[]; };
}
/**
* 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;
}
/**
* Type des paramètrès
*/
type IRIParameter = string | number;
export type IRIParameters = IRIParameter[] | IRIParameter;
/**
* Informations sur l'IRI d'une resource.
*
* P: types des paramètres.
*/
export class IRIMetadata
<P
extends
IRIParameters
>
{
public constructor(
private readonly testPattern: RegExp,
private readonly capturePattern: RegExp,
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) {
throw new Error(`Invalid path: ${path} does not match ${this.capturePattern}`);
}
if (matches.length == 2) {
return matches[1] as any;
}
return matches.slice(1) as any;
}
}
function getResourceType(that: any): string | null {
return (
(that !== null
&&
typeof that === 'object'
&&
typeof that[TYPE_PROPERTY] === 'string'
&&
that[TYPE_PROPERTY]) ||
null
);
}
/**
* Metadonnées d'une ressource.
*
* 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
,
P
extends
IRIParameters
>
{
public constructor(
public readonly type: T,
public readonly iri: IRIMetadata
<P>
,
private readonly requiredProperties: (keyof R)[],
private readonly types: ResourceMetadata
<any
,
any
,
P
>
[] = []
) {
this.types.unshift(this);
}
/**
* Vérifie si l'argument représente une ressource R ou dérivée.
*/
public isResource(that: any): that is R {
const type = getResourceType(that);
if (!type) {
return false;
}
return this.types.some(t => t.type === type);
}
/**
* Vérifie si une propriété est obligatoire dans la resource R.
*/
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);
}
/**
* Extrait les paramètres d'une IRI.
*/
public getIRIParameters(iri: IRI
<R>
): P {
return this.iri.parse(iri as any);
}
}
/**
* 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];
}
}
/**
* 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).generateIRI(parametersOrOptions as API[T]['iriParameters']);
} 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
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
%}
src/php/Resources/views/index.ts.twig
View file @
2afefafc
{%
extends
'@NgModelGenerator/_layout.ts.twig'
%}
{%
block
content
%}
export * from './common';
export * from 'irstea-ng-model/types';
export * from './resources';
export * from './repositories';
export * from './metadata';
...
...
src/php/Resources/views/metadata.ts.twig
View file @
2afefafc
...
...
@@ -6,7 +6,6 @@
{%
autoescape
false
%}
import { Injectable, Provider } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
AbstractAPIService,
AbstractResourceCache,
...
...
@@ -16,7 +15,8 @@ import {
LazyMetadataRegistry,
ResourceMetadata,
UUID,
} from './common';
} from 'irstea-ng-model/types';
import {
{%
for
repo
in
repositories
%}
{{
repo.usage
}}{%
if
not
loop.last
%}
,
{%
endif
%}
...
...
src/php/Resources/views/repositories.ts.twig
View file @
2afefafc
...
...
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { HttpResponseBase } from '@angular/common/http';
// @ts-ignore
import { AbstractRepository, Collection, IRI, RequestOptions, UUID } from '
./common
';
import { AbstractRepository, Collection, IRI, RequestOptions, UUID } from '
irstea-ng-model/types
';
import {
{%
for
name
in
repoImports
%}
{{
name
}}{%
if
not
loop.last
%}
,
{%
endif
%}
{%
-
endfor
%}
...
...
src/php/Resources/views/resources.ts.twig
View file @
2afefafc
...
...
@@ -3,7 +3,7 @@
{%
block
content
%}
{%
autoescape
false
%}
// @ts-ignore
import { DateTime, IRI, UUID } from '
./common
';
import { DateTime, IRI, UUID } from '
irstea-ng-model/types
';
/*********************************************************************************
* Ressources
...
...
src/ts/cache.service.spec.ts
deleted
100644 → 0
View file @
f742e178
import
{
HttpHeaderResponse
}
from
'
@angular/common/http
'
;
import
{
inject
,
TestBed
}
from
'
@angular/core/testing
'
;
import
*
as
_
from
'
lodash
'
;
import
{
forkJoin
,
Observable
}
from
'
rxjs
'
;
import
{
catchError
,
map
,
mergeMap
}
from
'
rxjs/operators
'
;
import
{
MarbleTestScheduler
}
from
'
../../../testing/marbles
'
;
import
{
Collection
,
IRI
,
IRI_PROPERTY
,
Resource
}
from
'
../../shared/models
'
;
import
{
safeForkJoin
}
from
'
../../shared/rxjs
'
;
import
{
IRIMismatchError
,
MissingIRIError
,
ResourceCache
,
ValueHolder
}
from
'
./cache.service
'
;
describe
(
'
cache.service
'
,
()
=>
{
interface
MyResource
extends
Resource
{
readonly
'
@id
'
:
IRI
<
MyResource
>
;
readonly
'
@type
'
:
'
MyResource
'
;
value
:
string
;
value2
?:
string
;
}
function
iri
(
x
:
string
):
IRI
<
MyResource
>
{
return
x
as
any
;
}
const
MY_IRI
=
iri
(
'
/bla/a
'
);
const
OTHER_IRI
=
iri
(
'
/bla/B
'
);
const
VALUES
:
{
[
name
:
string
]:
MyResource
}
=
{
a
:
{
'
@id
'
:
MY_IRI
,
'
@type
'
:
'
MyResource
'
,
value
:
'
foo
'
},
b
:
{
'
@id
'
:
MY_IRI
,
'
@type
'
:
'
MyResource
'
,
value
:
'
bar
'
},
c
:
{
'
@id
'
:
MY_IRI
,
'
@type
'
:
'
MyResource
'
,
value
:
'
bar
'
,
value2
:
'
quz
'
},
d
:
{
'
@id
'
:
OTHER_IRI
,
'
@type
'
:
'
MyResource
'
,
value
:
'
zig
'
},
};
let
scheduler
:
MarbleTestScheduler
<
any
>
;
beforeEach
(()
=>
{
scheduler
=
MarbleTestScheduler
.
create
(
VALUES
,
'
error
'
);
});
describe
(
'
ValueHolder
'
,
()
=>
{
let
holder
:
ValueHolder
<
any
>
;
beforeEach
(()
=>
{
holder
=
new
ValueHolder
<
MyResource
>
(
iri
(
'
/bla/a
'
));
});
it
(
'
should be created
'
,
()
=>
{
expect
(
holder
).
toBeTruthy
();
});
describe
(
'
.set()
'
,
()
=>
{
function
testSet
({
value
,
error
,
SET_M
}:
any
)
{
scheduler
.
withError
(
error
).
run
(({
expectObservable
})
=>
{
expectObservable
(
holder
.
set
(
value
)).
toBe
(
SET_M
);
});
}
it
(
'
should provide the value
'
,
()
=>
testSet
({
value
:
VALUES
.
a
,
SET_M
:
'
(a|)
'
,
}));
it
(
`should refuse value without
${
IRI_PROPERTY
}
`
,
()
=>
testSet
({
value
:
{},
error
:
new
MissingIRIError
(),
SET_M
:
'
#
'
,
}));
it
(
'
should refuse value with different @id
'
,
()
=>
testSet
({
value
:
{
'
@id
'
:
iri
(
'
bar
'
)
},
error
:
new
IRIMismatchError
(
MY_IRI
,
iri
(
'
bar
'
)),
SET_M
:
'
#
'
,
}));
it
(
'
should always points to the same instance
'
,
()
=>
{
forkJoin
(
holder
.
set
(
VALUES
.
a
),
holder
.
set
(
VALUES
.
b
)).
subscribe
(([
a
,
b
]:
MyResource
[])
=>
{
expect
(
a
).
toBe
(
b
);
});
});
});
describe
(
'
.update()
'
,
()
=>
{
it
(
'
should provide the value from the server
'
,
()
=>
scheduler
.
run
(({
cold
,
expectObservable
})
=>
{
const
REQ_M
=
'
---a|
'
;
const
UPD_M
=
'
--(a|)
'
;
const
request$
=
cold
(
REQ_M
);
expectObservable
(
holder
.
update
(
request$
)).
toBe
(
UPD_M
);
}));
it
(
'
should cancel pending requests
'
,
()
=>
{
const
LOCAL_VALUES
=
{
a
:
VALUES
.
a
,
b
:
VALUES
.
b
,
j
:
[
VALUES
.
b
,
VALUES
.
b
],
};
const
REQ1_M
=
'
---a|
'
;
const
REQ2_M
=
'
b|
'
;
const
UPDA_M
=
'
(j|)
'
;
const
REQ1_S
=
'
(^!)
'
;
const
REQ2_S
=
'
(^!)
'
;
scheduler
.
withValues
(
LOCAL_VALUES
).
run
(({
cold
,
expectObservable
,
expectSubscriptions
})
=>
{
const
request1$
=
cold
(
REQ1_M
);
const
request2$
=
cold
(
REQ2_M
);
expectObservable
(
safeForkJoin
([
//
holder
.
update
(
request1$
),
holder
.
update
(
request2$
),
])
).
toBe
(
UPDA_M
);
expectSubscriptions
(
request1$
.
subscriptions
).
toBe
(
REQ1_S
);
expectSubscriptions
(
request2$
.
subscriptions
).
toBe
(
REQ2_S
);
});
});
it
(
'
should propagate errors
'
,
()
=>
scheduler
.
run
(({
cold
,
expectObservable
})
=>
{
const
REQ_M
=
'
#
'
;
const
UPD_M
=
'
#
'
;
const
request$
=
cold
(
REQ_M
);
expectObservable
(
holder
.
update
(
request$
)).
toBe
(
UPD_M
);
}));
it
(
'
should restart on errors
'
,
()
=>
scheduler
.
run
(({
cold
,
expectObservable
})
=>
{
const
REQ1_M
=
'
#
'
;
const
UPD1_M
=
'
#
'
;
const
obs$
=
holder
.
update
(
cold
(
REQ1_M
));
expectObservable
(
obs$
).
toBe
(
UPD1_M
);
const
REQ2_M
=
'
-a|
'
;
const
UPD2_M
=
'
-a|
'
;
const
obs2$
=
holder
.
update
(
cold
(
REQ2_M
));
expectObservable
(
obs2$
).
toBe
(
UPD2_M
);
}));
});
describe
(
'
.listen()
'
,
()
=>
{
function
testListen