Code Generation Tool

CodeGen è il potente tool di Code Architects che permette di generare in pochi secondi più del 70% del codice che il developer scriverebbe manualmente. Con questo tool è possibile generare con un approccio assolutamente intuitivo (sia lato client che lato server) interi scenari applicativi, entità isomorfiche, enumerati, servizi e molto altro, definendo le metainformazioni in maniera guidata all’interno di file di tipo yaml. In questa sezione viene analizzato nello specifico il DSL (Domain Specific Language) utilizzato per modellare le applicazioni sviluppate con il CA-Platform. La libreria di riferimento per il suddetto DSL è “@ca-codegen/ng-aspnetcore-spa” ed i modelli YAML vengono definiti nella folder “codegenmodel” del progetto, mentre i template custom nella folder “codegentemplates”

YAML

YAML è un linguaggio di serializzazione dei dati human-readable. È comunemente usato per i file di configurazione. YAML ha una sintassi minimal che intenzionalmente differisce da XML. L’estensione dei file YAML è .yml Utilizza sia l’indentazione in stile Python per indicare l’annidamento, sia un formato più compatto che usa [] per gli array e {} per le mappe, rendendo YAML un superset di JSON. Sono consentiti tipi di dati custom, ma YAML codifica nativamente scalari (come stringhe, numeri interi e float), elenchi e array associativi. Questi tipi di dati si basano sui linguaggi di programmazione di alto livello. Per definire le coppie chiave-valore, la sintassi utilizzata è quella dei due punti. Il wrapping di spazi bianchi per stringhe a più righe è ispirato all’HTML. Gli elenchi possono contenere a loro volta altri elenchi nidificati che formano una struttura ad albero. Alcuni editor di codice sorgente come Visual Studio Code forniscono estensioni che rendono più semplice la modifica di YAML, come ripiegare strutture nidificate o evidenziare automaticamente errori di sintassi.

YAML accetta l ‘intero set di caratteri Unicode, ad eccezione di alcuni caratteri di controllo, e può essere codificato in UTF-8, UTF-16 e UTF-32. Di seguito le caratteristiche principali:

  • Whitespace indentation: usati per denotare le strutture. E’ necessario che le indentazioni siano rigorosamente rispettate, pena errori di definizione.

    entities:
    - name: Person
      type: entity
      description: Person entity
      isTrackable: false
      fields:
      - name: name
        type: string
        description: Customer name
        isKey: true
        isPublic: true
        variableAlias: nameAlias
        decorators:
          - type: aspect
            default:
              template: text
              label:
                key: name
                default: Nome
        validations:
          - type: mandatory
            message:
              key: field-mandatory
              default: Campo obbligatorio
          - type: custom
            name: containNumbers
            message:
              key: string-no-num
              default: La stringa non contiene un numero
  • Comment: i commenti iniziano con l’hash (#) e possono iniziare ovunque su una riga e continuare fino alla fine della stessa.

    # This is a comment
    # This is another comment
  • List: ciascun elemento di una lista è indicato da un trattino iniziale (-). È inoltre possibile specificare un elenco racchiudendo il testo tra parentesi quadre ([]) con ciascuna voce separata da virgole

    validations:
    - type: mandatory
      message:
        key: field-mandatory
        default: Campo obbligatorio
    - type: custom
      name: containNumbers
      message:
        key: string-no-num
        default: La stringa non contiene un numero
  • Object: la proprietà di un oggetto è rappresentata utilizzando l’associazione chiave-valore. Chiave e valore devono essere separate dai due punti (:). YAML richiede che i due punti siano seguiti da uno spazio in modo che i valori scalari possano generalmente essere rappresentati senza la necessità di essere racchiusi tra virgolette. Un oggetto può anche essere definito utilizzando le parentesi graffe ({}), con le proprietà separate da virgole, i valori testuali tra virgolette, senza la necessità di inserire spazi ed indentazioni (per la compatibilità con JSON)

    name: Person
    type: entity
    description: Person entity
    isTrackable: false
  • String: le stringhe sono normalmente non quotate, ma possono essere racchiuse tra virgolette doppie (“) o virgolette singole (‘). Tra virgolette, i caratteri speciali possono essere rappresentati con sequenze di escape in stile C che iniziano con una backslash ()

    description: This is a sample description

I file YAML supportati dal CA-Platform sono controllati e validati da uno schema JSON che “lavora” dietro le quinte. JSON Schema è una specifica per la strutturazione di dati basati su JSON. La compilazione dello YAML è dunque semplificata e guidata grazie allo schema JSON del repository @ca-codegen contenente i modelli e le definizioni dell’architettura. L’estensione di Visual Studio Code che permette l’abbinamento di uno schema a degli specifici file YAML è la seguente:

YAML from Redhat

Utilizzando Scarface per lo scaffold dell’applicazione, per usufruire dei vantaggi della compilazione guidata, sarà sufficiente installare l’estensione. Per maggior dettagli su Scarf-Ace, visita la sezione “Scarface”

Il CA-Platform supporta 4 tipologie di layer: application, domain, scenario e shared. E’ possibile definire ciascun layer in file YAML distinguendoli utilizzando la proprietà “type”. Le proprietà utilizzabili varieranno a seconda del type specificato. Installando la seguente estensione, sarà possibile lanciare automaticamente la generazione del codice ad ogni modifica effettuata su un file YAML:

Trigger Task on save

Service

Un’applicazione distribuita è tipicamente composta da vari microservizi che interagiscono tra loro, dividendosi le funzionalità esposte all’esterno del back-end. È possibile generare un intero microservizio a partire da un singolo file `yaml` che lo descrive. Questo file ha la seguente struttura:

name: Customers
type: service
description: Customers service
namespace: Ca.Crm.Customers
messaging: ...
components: ...
entities: ...
contracts:
  operations: ...
  dto: ...
  services: ...
mappings: ...
partials: ...
services: ...

Impostando “service” come “type” nel file YAML, sarà possibile utilizzare le seguenti proprietà:

name: Nome del microservizio. Deve essere una stringa in PascalCase, ovvero con la prima lettera maiuscola seguita da eventuali caratteri alfabetici (esempi: ‘Suite’, ‘Users’, ‘ModelBase’). Utilizzando Scarface per la generazione del layer, l’unico vincolo da ripettare sarà quello che il name richiesto dovrà adottare la sintassi kebab-case, ovvero stringa in lowercase con ogni parola separata da un trattino (esempi: ‘suite’, ‘users’, ‘model-base’) ed il tool si occuperà di convertirlo in PascalCase come valore del name stesso (esempi: ‘Suite’, ‘ModelBase’), ed in camelCase come nome del file YAML (esempi: ‘suite.yml’, ‘modelBase.yml’).

type: Tipologia di layer

description: Descrizione del layer.

namespace: Namespace del microservizio. Deve essere un set di parole separate da punti. Esempio: ‘Prefix.Project.Service’

messaging:
Descrive una lista dei messaggi che possono essere ricevuti dal microservizio e degli handlers che li gestiranno.

components: Descrive una lista di componenti infrastrutturali utilizzati dal microservizio.

entities: Descrive le classi rappresentanti gli oggetti persistiti nel database (Persistent Objects).

contracts: Contiene le definizioni dell’API e dei Consumer Driven Contracts esposti dal microservizio (procedure e dto).

mappings: Lista dei mapping tra entità di dominio e dto.

partials: Contiene i riferimenti a file yaml parziali, che vanno ad aggiungersi alle informazioni modellate nello specifico yaml.

services: Definisce la lista dei servizi che è possibile invocare dal servizio corrente, mediante service-to-service invocation. Per ogni servizio incluso, Codegen genererà una classe con i metodi atti ad invocare i suoi metodi, che potranno essere consumati mediante Dependency Injection della relativa interfaccia (anch’essa generata).

Application

L’application layer rappresenta il container principale dell’applicazione. L’applicazione è costituita/descritta da domini applicativi. Le meta-informazioni dell’applicazione devono essere definite in un file YAML con il nome dell’applicazione in camelCase (applicationName.yml) nella folder model contenuta in codegen. Utilizzando Scarface non sarà necesario ricordare la posizione e la naming convention del file YAML dell’applicazione, ma basterà rispondere al prompt di Scarface con il nome dell’applicazione in kebab-case (application-name). Nell’esempio seguente viene definito lo YAML di una applicazione chiamata Suite:

name: Suite
type: application
author: CodeArchitects
description: The registry management system application
baseApiUrl: ca/suite
services: ...

Impostando “application” come “type” nel file YAML, sarà possibile utilizzare le seguenti proprietà:

name: Nome del layer. Deve essere una stringa in PascalCase, ovvero con la prima lettera maiuscola seguita da eventuali caratteri alfabetici (esempi: ‘Suite’, ‘Users’, ‘ModelBase’). Utilizzando Scarface per la generazione del layer, l’unico vincolo da ripettare sarà quello che il name richiesto dovrà adottare la sintassi kebab-case, ovvero stringa in lowercase con ogni parola separata da un trattino (esempi: ‘suite’, ‘users’, ‘model-base’) ed il tool si occuperà di convertirlo in PascalCase come valore del name stesso (esempi: ‘Suite’, ‘ModelBase’), ed in camelCase come nome del file YAML (esempi: ‘suite.yml’, ‘modelBase.yml’).

type: Tipologia di layer

author: Nome dell’autore del progetto. Deve essere una stringa con la prima lettera maiuscola seguita da eventuali caratteri alfanumerici. Esempi: ‘Bross’, ‘Bross31’

description: Descrizione del layer.

baseApiUrl: DEPRECATED

services: Lista dei microservizi che è possibile interrogare da tutti i livelli dell’applicazione (tutti i domini e tutti gli scenari)

Domain

Il domain layer rappresenta il container più prossimo all’application layer e delinea uno specifico contesto applicativo, ovvero un particolare contesto in cui l’applicazione opera. Ogni dominio è rappresentato dagli scenari. Le meta-informazioni del dominio devono essere definite in un file YAML con il nome del dominio in camelCase preceduto dal nome dell’applicazione di riferimento sempre in camelCase e separati da un trattino (applicatioName-domainName.yml) nella folder model contenuta in codegen. Utilizzando Scarface non sarà necesario ricordare la posizione e la naming convention del file YAML del dominio, ma basterà rispondere al prompt di Scarface con il nome del dominio in kebab-case (domain-name) e scegliendo da una lista l’applicazione di riferimento. Nell’esempio seguente viene definito lo YAML di un dominio dell’applicazione Suite chiamato Crm:

name: Crm
type: domain
application: Suite
description: Customer relationship management domain
services: ...

Impostando “domain” come “type” nel file YAML, sarà possibile utilizzare le seguenti proprietà:

name: Nome del layer. Deve essere una stringa in PascalCase, ovvero con la prima lettera maiuscola seguita da eventuali caratteri alfabetici (esempi: ‘Suite’, ‘Users’, ‘ModelBase’). Utilizzando Scarface per la generazione del layer, l’unico vincolo da ripettare sarà quello che il name richiesto dovrà adottare la sintassi kebab-case, ovvero stringa in lowercase con ogni parola separata da un trattino (esempi: ‘suite’, ‘users’, ‘model-base’) ed il tool si occuperà di convertirlo in PascalCase come valore del name stesso (esempi: ‘Suite’, ‘ModelBase’), ed in camelCase come nome del file YAML (esempi: ‘suite.yml’, ‘modelBase.yml’).

type: Tipologia di layer

application: Riferimento all’applicazione di appartenenza (name specificato nello YAML dell’applicazione).

description: Descrizione del layer.

services: Lista dei microservizi che è possibile interrogare dallo specifico dominio e da tutti i suoi scenari

Scenario

Lo scenario layer rappresenta il container più atomico dell’applicazione e descrive un particolare contesto applicativo legato ad uno specifico dominio (esempio: scenario: anagrafica-persona -> dominio: anagrafiche). A ciascun scenario è legata una Activity di Workflow (navigazionale). Si tratta (nello specifico) di una macchina a stati costituita da stati/nodi rappresentanti le viste dello scenario effettivamente navigabili. Il caricamento di uno scenario corrisponderà all’avvio di un task (identificato da un GUID assegnato dinamicamente) che terminerà solamente al verificarsi di navigazioni assolute (routerLink o router service) [vedi paragrafo Navigation per maggiori dettagli]. Le meta-informazioni dello scenario devono essere definite in un file YAML con il nome dello scenario in camelCase preceduto dal nome del dominio di appartenenza in camelCase, a sua volta preceduto dal nome dell’applicazione di riferimento sempre in camelCase e separati da un trattino (applicatioName-domainName-scenarioName.yml) nella folder model contenuta in codegen. Utilizzando Scarface non sarà necesario ricordare la posizione e la naming convention del file YAML dello scenario, ma basterà rispondere al prompt di Scarface con il nome dello scenario in kebab-case (scenario-name) e scegliendo da una lista l’applicazione ed il dominio di riferimento. Nell’esempio seguente viene definito lo YAML di uno scenario Customers appartenente al dominio Crm dell’applicazione Suite:

name: Customers
type: task
description: Customers scenario
domain: Crm
activity: CustomersActivity
services: ...

Impostando “task” come “type” nel file YAML, sarà possibile utilizzare le seguenti proprietà:

name: Nome del layer. Deve essere una stringa in PascalCase, ovvero con la prima lettera maiuscola seguita da eventuali caratteri alfabetici (esempi: ‘Suite’, ‘Users’, ‘ModelBase’). Utilizzando Scarface per la generazione del layer, l’unico vincolo da ripettare sarà quello che il name richiesto dovrà adottare la sintassi kebab-case, ovvero stringa in lowercase con ogni parola separata da un trattino (esempi: ‘suite’, ‘users’, ‘model-base’) ed il tool si occuperà di convertirlo in PascalCase come valore del name stesso (esempi: ‘Suite’, ‘ModelBase’), ed in camelCase come nome del file YAML (esempi: ‘suite.yml’, ‘modelBase.yml’).

type: Tipologia di layer

domain: Riferimento al dominio di appartenenza (name specificato nello YAML del dominio).

description: Descrizione del layer.

activity: Riferimento all’activity del workflow dello scenario (definita nel file nomnoml abbinato “scenario-repo.nomnoml”)

services: Lista dei microservizi che è possibile interrogare dallo specifico dominio e da tutti i suoi scenari

Gateway

L’application Gateway è un servizio di bilanciamento del carico del traffico Web che consente di gestire il traffico verso i microservizi. Il gateway consente di prendere decisioni relative al routing basate su altri attributi di una richiesta HTTP, ad esempio il percorso dell’URI o le intestazioni host. Ad esempio, è possibile eseguire il rounting del traffico in base all’URL in ingresso. Lo YAML di tipo gateway permette di generare un application gateway con ENVOY e di aggiungerne/rimuovere dinamicamente microservizi.

name: SPA
type: gateway
description: SPA gateway
port: 10000
services: ...

name: Nome del gateway

type: Tipologia di layer

description: Descrizione del layer.

port: Porta locale sulla quale lanciare il gateway

services: Lista dei microservizi esposti sul gateway

NOMNOML

NOMNOML è un tool per disegnare diagrammi UML basati su una semplice sintassi. Nella platform permette di definire gli stati del workflow navigazionale di uno scenario e le relative transizioni.

[<activity>CustomersActivity|
  [<start>start]
  [<state>browse]
  [<state>add]
  [<state>update]

  [start] browse -> [browse]
  [browse] add -> [add]
  [browse] update -> [update]
  [browse] foo -> [update]
]

Utilizzando Scarface (per la generazione dello scenario e degli stati) non sarà necessaria la modifica dei file NOMNOML. E’ tuttavia possibile editare questi tipi di file rispettando la seguente sintassi:

  • La definizione dell’activity (stati ed associazioni) deve avvenire all’interno di parentesi quadre […]
  • La prima informazione da inserire tra le parentesi quadre è il nome dell’activity dello scenario (in PascalCase) preceduta dall’attributo e seguita da una pipe “|”. Il nome assegnato all’activity deve essere univoco in tutto il progetto (non devono esserci altri file NOMNOML che definiscano una activity con lo stesso nome)
  • Tra gli stati dello scenario ce ne deve essere sempre uno di partenza: lo stato start. Tale stato deve essere preceduto dall’attributo e rappresenta il nodo di partenza del workflow navigazionale. Quando si avvia un nuovo task tramite navigazione assoluta (es. routerLink: [‘suite’,’crm’, ‘customers’]), si dovrà obbligatoriamente passare dal nodo start; quando invece si effettua una navigazione relativa (es. this.navigateWithReturn(…)) non bisognerà passarci
  • Tutti gli altri stati del workflow devono essere preceduti dall’attributo
  • A ciascuno stato (incluso start) sarà legata una componente adornata con il decoratore @ActivityComponent. Con l’applicazione di questo decorator, la classe non sarà più una semplice componente, ma sarà una componenta legata ad una specifica activity. Per maggior dettagli sul decoratore @ActivityComponent, visita la sezione “Patterns”
  • Con la sintassi “[x] action -> [y]” si definisce una transizione tra due stati dello stesso scenario, dove “x” rappresenta lo stato di partenza, “y” lo stato di arrivo ed “action” il nome del nuovo command da eseguire nello stato “x” per navigare verso lo stato “y”.

La generazione dell’activity definita all’interno del file NOMNOML, produrrà una componente per ciascuno stato, come nell’esempio seguente (scenario: agenda, stati: start, browse):

Ciascuno stato sarà composto da un file html, un file scss, un file ts ed un file spec.ts. Nel file html andrà inserito il template legato allo stato, mentre nel file ts la logica dello stesso. Il file typescript sarà simile al seguente (esempio con lo stato browse):

@ActivityComponent({ extends: Base.BrowseComponent })
@Component({ templateUrl: 'browse.html', providers: [Base.AgendaServices] })
export class BrowseComponent extends Base.BrowseComponent implements IOnInit {

  public constructor(
    injector: Injector,
    services: Base.AgendaServices
  ) {
    super(injector, services);
  }

  public async onInit(params: {}) {

  }

}

Il file *.spec.ts conterrà gli hook per effettuare i test unitari sul relativo componente/stato dello scenario. Il file sarà già incluso nella suite di test applicativa, prevederà già la configurazione del modulo di testing e sarà simile al seguente (esempio con lo stato browse):

// --inject:IMPORTS--
describe('Suite/Crm/Customers/browse action', () => {
  let component: BrowseComponent;
  let fixture: ComponentFixture<BrowseComponent>;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        // --inject:TESTIMPORTS--
        ComponentsModule,
        HttpClientModule,
        RouterTestingModule,
        TranslateModule.forRoot(),
        // --inject:TESTIMPORTS--
      ],
      declarations: [
        // --inject:TESTDECLARATIONS--
        // --inject:TESTDECLARATIONS--
        BrowseComponent
      ],
      providers: [
        // --inject:TESTPROVIDERS--
        CommandDispatcherService,
        ContextService,
        DataContextService,
        SerializerService,
        { provide: ShHttp, useClass: ShAuthHttp },
        // --inject:TESTPROVIDERS--
        InvoicesActivity, InvoicesDelegates
      ]
    }).compileComponents();
  }));
  beforeEach(() => {
    // --inject:BEFOREACH-BEGIN--
    fixture = TestBed.createComponent(BrowseComponent);
    component = fixture.componentInstance;
    // --inject:BEFOREACH-BEGIN--
    component.activity.payload = <CustomersPayload>{};
    component.delegates.getUniqueIdentifier = () => {
      const subject = new Subject<string>();
      setTimeout(() => subject.next('8aca28c1-9754-446e-8930-92cb3d66be66'), 100);
      return subject;
    };
    component.onInit = () => { };
    component.onInit({});
    fixture.detectChanges();
    // --inject:BEFOREACH-END--
    // --inject:BEFOREACH-END--
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Il file *.scss potrà contenere lo stile incapsulato per il componente/stato ad esso associato. Sconsigliamo l’utilizzo di questi file per evitare di avere uno stile differente per ciascuna pagina dell’applicazione. Consigliamo invece l’utilizzo di componenti con stile già definito (come la libreria di componenti del CA Platform), in modo tale da limitarsi a montare le interfacce con i componenti messi a disposizione invece di scriverne ogni volta lo stile. E’ tuttavia possibile utilizzare questo file, legandone lo style nell’attributo styleUrls del decoratore Component, come di seguito (esempio con lo stato browse):

@ActivityComponent({ extends: Base.BrowseComponent })
@Component({ templateUrl: 'browse.html', providers: [Base.InvoicesServices], styleUrls: ['./browse.scss'] })
export class BrowseComponent extends Base.BrowseComponent implements IOnInit {

Services Inclusion

La proprietà ‘services’ permette di importare (all’interno del container nel quale è applicato) tutte le informazioni definite in un container di tipo Service. Importando un service in un container di tipo “application”, “domain”, “scenario” o “task”, il generatore di codice genererà i dto nel file ‘nome-servizio.ts’ della cartella models (del container stesso) e i metodi delegati nel file ‘nome-servizio_service.ts’ della cartella services (del container) e sarà dunque possibile accedere ai metodi proxy generati tramite il servizio delegates (this.delegates.nomeServizio.nomeMetodo). Importando un service in un container di tipo “gateway”, quest ultimo verrà esposto sull’application gateway.

services:
  - service: Store
  - service: Logistics
  - ...

service: Nome identificativo del container di tipo Service da importare

Messaging

Questa sezione configura la funzione di messaging (pub/sub) del microservizio. Qui, viene indicata la lista dei messaggi che possono essere ricevuti dal microservizio e gli handlers che li gestiranno.

messaging:
  handlers:
    - name: MyMessageHandler
      description: Handler of MyMessage1 and MyMessage2
      messages:
        - MyMessage1
        - MyMessage2
  messages:
    - name: MyMessage1
      description: My message 1
      fields:
        - name: id
          description: The id of the message
          type: uuid
        - name: name
          description: The name of the message
          type: string
    - name: MyMessage2
      description: My message 2
      fields:
        - name: id
          description: The id of the message
          type: uuid
        - name: name
          description: The name of the message
          type: string

I messaggi (MyMessage1, MyMessage2) dovranno esistere anche in altri microservizi che provvederanno a pubblicarli, così che il message broker possa inoltrarli al microservizio di cui questo yaml fa parte.

Components

Questa sezione dello yaml contiene la lista dei componenti di infrastruttura (ad esempio: database, messagebus, ecc.) utilizzati dal microservizio. Ciascun componente deve avere un tipo, un nome ed un provider. Il provider è l’implementazione scelta del tipo di componente (ad esempio: Sql Server come database, RabbitMQ come message broker, ecc.). Al momento, sono supportati 3 diversi tipi di components:

database: Descrive il database utilizzato dal microservizio. Includendo un component di tipo ‘database’, verrà creato un container docker contenente un’istanza del database scelto, insieme ad un volume che ospiterà i suoi dati per persisterli su disco.

components:
  - name: sqlserver
    type: database
    provider: sqlserver

I provider per il tipo ‘database’ attualmente supportati sono:

  • sqlserver: SQL Server

messagebus: Descrive il message bus utilizzato dal microservizio. Includendo un component di tipo ‘messagebus’, verrà creato un container docker contenente un’istanza del message broker scelto.

components:
  - name: rabbit
    type: messagebus
    provider: rabbitmq

I provider per il tipo ‘messagebus’ attualmente supportati sono:

  • rabbit: RabbitMQ
  • redis: Redis Streams

statestore: Descrive lo state store utilizzato dal microservizio. Includendo un component di tipo ‘statestore’, verrà creato un container docker contenente un’istanza dello state store scelto.

components:
  - name: redis
    type: statestore
    provider: redis

I provider per il tipo ‘statestore’ attualmente supportati sono:

  • redis: Redis

Contracts

Contiene le definizioni dell’API e dei Consumer Driven Contracts esposti dal microservizio (procedure e dto).

contracts:
  operations: ...
  dto: ...
  services:
    - name: MyController
      description: Custom controller
      operations: ...
      dto: ...

operations: codegen-service-operations-explanation

dto: Definisce la lista dei DTOs (Data Transfer Objects) utilizzati all’interno delle operations.

services: Definisce una lista di controller, composti da dto ed operations

  • name: Nome del controller (in PascalCase)
  • description: Descrizione del controller
  • operations: Definisce una lista di operazioni esposte dal controller del microservizio
  • dto: Definisce la lista dei DTOs utilizzati all’interno delle operations del controller

DTO

La proprietà ‘dto’ permette di definire una lista di DTO (Data Transfer Object) e/o una lista di enumerati relativi al microservizio.

dto:
- name: Person
  type: entity
  description: Person entity
  isTrackable: true
  isAbstract: true
- name: Customer
  type: entity
  description: Customer entity
  ancestor: Person
  disableMappingTestGeneration: true
  enableGetVariablesGeneration: true
  resource: class://Crm/Customers
  fields: ...
  validations: ...
  warnings: ...
- name: JobState
  type: enum
  description: Customer job state
  enumeratorList:
    - name: Started
      description: Job started
    - name: Fired
      description: Job fired
      value: 0

Le classi generate lato Typescript e lato C#, saranno isomorfiche. Trasferendole da client a server e viceversa attraverso le operations, i dto manterranno la loro identità e non sarà necessario effettuare alcuna operazione di conversione. Ciascun dto potrà essere composto dalle seguenti proprietà:

name: Nome del dto. Il nome deve rispettare la notazione PascalCase (NomeCampo).

type: Tipologia di oggetto (entity o enum)

description: Descrizione del dto

isTrackable: Se impostato a true, tale impostazione renderà il dto Trackable abilitandone il Change Tracking

isAbstract: Se impostato a true, rende la classe astratta

ancestor: Con questa proprietà è possibile impostare come classe padre, una delle classi definite nello stesso file YAML (ancestor: NomeClasse).

disableMappingTestGeneration: Se impostato a true, disabilita la generazione dei test unitari del mapping per il dto in questione

enableGetVariablesGeneration: Se impostato a true, crea un metodo a livello di classe che restituisce un dizionario in cui la chiave è il nome della proprietà della classe ed il valore è il valore della proprietà stessa

resource: Resource name da associare alla classe. Per maggior dettagli sulle resource, policy e claims, visita la sezione “Authorization”

fields: Lista di proprietà della classe (concetto approfondito nel paragrafo successivo “DTO Fields”)

enumeratorList: Proprietà presente solo con “type=enum”. Permette di definire la lista dei valori dell’enumerato, fornendo un nome in PascalCase ed un eventuale value

validations: Lista di regole di validazione da applicare alla classe (concetto approfondito nel paragrafo successivo “DTO Fields”)

warnings: Lista di regole di warning da applicare alla classe (concetto approfondito nel paragrafo successivo “DTO Fields”)

DTO Fields

La proprietà “fields” di una classe (proprietà “dto”), permette di definire la lista delle proprietà che definiscono le caratteristiche di un dto.

dto:
- name: Customer
  ...
  fields:
  - name: code
    type: numeric
    description: Codice readonly
    isNullable: true
    isReadOnly: true
  - name: name
    type: string
    description: Customer name
    isKey: true
    isPublic: true
    variableAlias: nameAlias
    decorators:
      - type: aspect
        default:
          template: text
          label:
            key: name
            default: Nome
    validations:
      - type: mandatory
        message:
          key: field-mandatory
          default: Campo obbligatorio
      - type: custom
        name: containNumbers
        message:
          key: string-no-num
          default: La stringa non contiene un numero
  - name: email
    type: string
    description: Customer email
    decorators:
      - type: aspect
        default:
          template: textarea
          label: Email
    validations:
      - type: pattern
        pattern: ^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$
        message: Mail format error
    warnings:
      - type: mandatory
        message: It's advisable to populate the field
  - name: manager
    type: Customer
    description: Customer Work Manager

NOTE IMPORTANTI: Ciascun dto eredita implicitamente tutte le caratteristiche della classe EntityDTO del platform. Tale classe, fornisce tra le proprietà una proprietà id (id lato typescript, Id lato C#) di formato Guid (string lato typescript, System.Guid lato C#). E’ imporante dunque non definire nuove proprietà con il nome “id” all’interno delle classi e tenerne conto nei mapping (lato C#, mappando l’eventuale proprietà equivalente dell’entità del DB o inizializzando la proprietà almeno con un nuovo Guid) e nelle inizializzazioni (lato Typescript, utilizzando UUID.UUID()). La proprietà “id” (come indicato poco più sopra), è implicitamente impostata come chiave primaria (isKey: true) della classe; questo (per l’Object Identity, che il platform applica automaticamente) permette di distinguere una istanza della classe descritta, da un’altra istanza. Se dunque, due istanze della stessa classe avranno lo stesso id, faranno riferimento alla medesima istanza. Tra gli errori più comuni nell’utilizzo del CA-Platform, c’è quella di ricevere sul client istanze di una medesima classe con lo stesso guid (generalmente guid empty) e vederle come identiche. E’ importante dunque avere sempre un chiavi differenti per le stesse istanze.

name: Nome della proprietà della classe. Il nome deve rispettare la notazione camelCase (nomeCampo).

type: Tipologia della proprietà. Può essere scelto dalla lista dei suggerimenti o può essere identificata in una entità definita nello stesso YAML. La lista dei suggerimenti invece, presenta dei “tipi” rappresentanti una sorta di astrazione rispetto al linguaggio nel quale vengono generati. Di seguito tutti i tipi:

  • boolean: Boolean (C#) – boolean (Typescript)
  • byte: System.Byte (C#) – number (Typescript)
  • date: DateTime? (C#) – Date (Typescript)
  • datetime: DateTime? (C#) – DateTime (Typescript)
  • decimal: decimal (C#) – number (Typescript)
  • float: float (C#) – number (Typescript)
  • integer: Int32 (C#) – number (Typescript)
  • numeric: Double (C#) – number (Typescript)
  • sequence: Int64 (C#) – number (Typescript)
  • string: String (C#) – string (Typescript)
  • time: DateTime? (C#) – Date (Typescript)
  • uint8array: System.Byte[] (C#) – Uint8Array (Typescript)
  • uuid: System.Guid (C#) – string (Typescript)

description: Descrizione del field

isArray: Se impostato a true, rende la proprietà una lista della tipologia indicata nel campo “type”

isAbstract: Se impostato a true, rende la proprietà astratta

isStatic: Se impostato a true, rende la proprietà statica

isNullable: [DEPRECATED] – usare la proprietà isOptional

isOptional: Se impostato a true, rende il campo nullable lato C#

isReadOnly: Se impostato a true, rende il campo readonly, rimuovendo i relativi setter lato C# e lato Typescript

isKey: Se impostato a true, rende il campo chiave primaria (insieme alle altre key). Questa proprietà permette di applicare i concetti dell’Object Identity alla classe, rendendo esattamente identici gli oggetti che avranno lo stesso valore della proprietà impostata come key

isPublic: Se impostato a false, rende il campo private

resource: Resource name da associare al field. Per maggior dettagli sulle resource, policy e claims, visita la sezione “Authorization”

variableAlias: OBSOLETE

decorators: Decoratore Typescript che permette di applicare il paradigma dell’Aspect Programming ai field dell’entità, associandone delle meta-informazioni. Utilizzando il componente sh-form-control con come model binding l’istanza della classe e come prop binding il nome del field in questione, ad applicazione avviata, il suddetto componente leggerà le metainformazioni applicate sul field della classe e dinamicamente allocherà il corretto template e l’eventuale label associata. Il decoratore trattato è il decoratore @Aspect. Per maggior dettagli sul “Context”, visita la sezione “Metadati & Decoratori”

  • type: aspect

  • default/browse/edit: Generalmente viene utilizzata la voce “default” per definire le regole Aspect indipendentemente dal “Context”. Nel caso in cui si utilizzi il “Context”, è possibile definire una regola Aspect per il context “Browse” e per il context “Edit”. Per maggior dettagli sui decoratori @Aspect e @Validation, visita la sezione “Metadati & Decoratori”

    • label: Label da associare al template del campo. Può essere una semplice stringa, od una stringa localizzata. La stringa localizzata ha le seguenti proprietà:

      • key: Chiave di traduzione da inserire nel dizionario della lingua (generalmente inserito negli assets [es. it.json])
      • default: Stringa di fallback, in caso di assenza della chiave
    • template: Aspect key del componente da associare al field. L’Aspect Key è indicata al top di ogni playground dei componenti (es. Textarea => Aspect Key: textarea)

validations: Decoratore Typescript che permette di applicare regole di validazione ai field dell’entità, associandone delle meta-informazioni. Utilizzando il componente sh-form-control con come model binding l’istanza della classe e come prop binding il nome del field in questione, ad applicazione avviata, il suddetto componente leggerà le metainformazioni applicate sul field della classe e dinamicamente applicherà la regola di validazione e l’eventuale messaggio di errore associato. Il decoratore trattato è il decoratore @Validation. Per maggior dettagli sui decoratori @Aspect e @Validation, visita la sezione “Metadati & Decoratori”

  • type: Tipologia di regola di validazione. E’ possibile scegliere tra “mandatory” (il campo sarà obbligatorio), “pattern” (il campo deve rispettare la regular expression indicata) o “custom” (validazione custom creata nel progetto).
  • name: Campo abilitato solo scegliendo come tipologia “custom”. Indica il nome del validatore custom. I validatori custom vanno inseriti nel file “validators.ts” situato nella folder “clientsrcappnome-appservices”. Per maggior dettagli sui custom validators, visita la documentazione ufficiale di “Angular”
  • pattern: Campo abilitato solo scegliendo come tipologia “pattern”. Indica la regular expression da applicare come regola sul campo.
  • value: Campo abilitato solo scegliendo come tipologia “custom”. Campo non obbligatorio che indica l’eventuale valore statico da passare al validatore.
  • message: Messaggio di errore da associare al template del campo. Il campo comparirà solo in caso di regola di validazione non rispettata. Può essere una semplice stringa, od una stringa localizzata. La stringa localizzata ha le seguenti proprietà:
    • key: Chiave di traduzione da inserire nel dizionario della lingua (generalmente inserito negli assets [es. it.json])
    • default: Stringa di fallback, in caso di assenza della chiave

warnings: Decoratore Typescript che permette di applicare regole di validazione NON bloccanti ai field dell’entità, associandone delle meta-informazioni. Utilizzando il componente sh-form-control con come model binding l’istanza della classe e come prop binding il nome del field in questione, ad applicazione avviata, il suddetto componente leggerà le metainformazioni applicate sul field della classe e dinamicamente applicherà la regola di validazione NON bloccante e l’eventuale messaggio di warning associato. Il decoratore trattato è il decoratore @Warning. Per maggior dettagli sui decoratori @Aspect e @Validation, visita la sezione “Metadati & Decoratori”

  • type: Tipologia di regola di validazione. E’ possibile scegliere tra “mandatory” (il campo sarà obbligatorio), “pattern” (il campo deve rispettare la regular expression indicata) o “custom” (validazione custom creata nel progetto).
  • name: Campo abilitato solo scegliendo come tipologia “custom”. Indica il nome del validatore custom. I validatori custom vanno inseriti nel file “validators.ts” situato nella folder “clientsrcappnome-appservices”. Per maggior dettagli sui custom validators, visita la documentazione ufficiale di “Angular”
  • pattern: Campo abilitato solo scegliendo come tipologia “pattern”. Indica la regular expression da applicare come regola sul campo.
  • value: Campo abilitato solo scegliendo come tipologia “custom”. Campo non obbligatorio che indica l’eventuale valore statico da passare al validatore.
  • message: Messaggio di errore da associare al template del campo. Il campo comparirà solo in caso di regola di validazione non rispettata. Può essere una semplice stringa, od una stringa localizzata. La stringa localizzata ha le seguenti proprietà:
    • key: Chiave di traduzione da inserire nel dizionario della lingua (generalmente inserito negli assets [es. it.json])
    • default: Stringa di fallback, in caso di assenza della chiave

Operations

Definisce una lista di operazioni, ovvero actions, esposte dal servizio e invocabili dall’esterno tramite richieste HTTP.

operations:
- name: getCustomers
  type: http_auto
  description: Recupera i customers
  disableContextAttach: true
  parameters: ...

Le operations lato client sono invocabili mediante l’oggetto “delegates” (this.delegates.nomeOperation) ad ogni livello. I delegates ereditano i metodi delegates dei container genitori. Una operation è composta dalle seguenti proprietà:

name: Nome dell’operation.. Il nome deve rispettare la notazione camelCase (nomeOperation).

type: Tipologia di operation (http verb, subscription, …). E’ possibile scegliere tra le seguenti tipologie:

  • http_delete: Il metodo DELETE richiede che il server di origine elimini la risorsa identificata dall’URI della richiesta
  • http_get: Il metodo GET permette di recuperare qualsiasi informazione (sotto forma di entità) identificata dall’URI della richiesta
  • http_head: Il metodo HEAD è identico a GET tranne per il fatto che il server NON DEVE restituire il corpo del messaggio nella risposta
  • http_jsonp: OBSOLETE
  • http_options: Il metodo OPTIONS rappresenta una richiesta di informazioni sulle opzioni di comunicazione disponibili sulla catena di richiesta / risposta identificata dalla Request-URI
  • http_patch: Il metodo PATCH viene utilizzato per applicare modifiche parziali alle entità
  • http_post: Il metodo POST viene utilizzato per richiedere che il server di origine accetti l’entità racchiusa nella richiesta come nuovo subordinato della risorsa identificata dall’URI della richiesta nella riga della richiesta
  • http_put: Il metodo PUT richiede che l’entità racchiusa venga archiviata nell’URI di richiesta fornito. Se l’URI della richiesta fa riferimento a una risorsa già esistente, l’entità racchiusa DOVREBBE essere considerata come una versione modificata di quella che risiede sul server di origine
  • http_auto: Tipologia di verbo http di default (POST)
  • signalr_subscription: codegen-operations-signalr_subscription-explanation

description: Descrizione del metodo del controller

disableContextAttach: Disabilita l’attach del data context. Impostando il valore a true, feature come l’Object Identity verranno disabilitate.

auth-policies: Applica l’attributo “Authorize” con “Policy” sul metodo del controller C#.

resource: OBSOLETE

mode: sync: metodo del controller sincrono – async (default): metodo del controller asincrono

parameters: Lista dei parametri del metodo

Operation Parameters

La proprietà “parameters” di una operation (proprietà “operations”), permette di definire la lista dei parametri del metodo del controller.

- name: getCustomers
  type: http_get
  description: Recupera i customers
  parameters:
    - name: customers
      type: Customer
      description: Customers
      direction: retval
      isArray: true
- name: getCustomerById
  type: http_get
  description: Recupera il customer per identificativo
  parameters:
    - name: id
      type: uuid
      description: Identificativo del customre
      direction: in
    - name: customer
      type: Customer
      description: Customer
      direction: retval
- name: updateCustomer
  type: http_post
  description: Applica le modifiche al customer
  parameters:
    - name: customer
      description: Customer da modificare
      direction: in
      type: Customer
    - name: success
      description: Risultato operazione
      direction: retval
      type: boolean
- name: updateCustomerName
  type: http_put
  description: Cambia il nome del customer
  parameters:
    - name: customer
      type: Customer
      description: Customer per il quale cambiare il nome
      direction: inout
    - name: success
      type: boolean
      description: Risultato operazione
      direction: retval
    - name: message
      type: string
      description: Messaggio di ritorno
      direction: out

name: Nome del parametro. Il nome deve rispettare la notazione camelCase (nomeParametro).

description: Descrizione del parametro

type: Tipologia della proprietà. Può essere scelto dalla lista dei suggerimenti o può essere identificata in una entità definita nello stesso YAML, in uno YAML shared incluso o in un container genitore (lo scenario eredita tutte le entities del dominio e dell’applicazione, come il dominio eredita tutte le entities dell’applicazione). La lista dei suggerimenti invece, presenta dei “tipi” rappresentanti una sorta di astrazione rispetto al linguaggio nel quale vengono generati. Di seguito tutti i tipi:

  • boolean: Boolean (C#) – boolean (Typescript)
  • byte: System.Byte (C#) – number (Typescript)
  • date: DateTime? (C#) – Date (Typescript)
  • datetime: DateTime? (C#) – DateTime (Typescript)
  • decimal: decimal (C#) – number (Typescript)
  • float: float (C#) – number (Typescript)
  • integer: Int32 (C#) – number (Typescript)
  • numeric: Double (C#) – number (Typescript)
  • sequence: Int64 (C#) – number (Typescript)
  • string: String (C#) – string (Typescript)
  • time: DateTime? (C#) – Date (Typescript)
  • uint8array: System.Byte[] (C#) – Uint8Array (Typescript)
  • uuid: System.Guid (C#) – string (Typescript)

direction: Definisce la tipologia di parametro. Può essere identificato in una delle seguenti tipologie:

  • in: Parametro inserito nella “busta” (envelop) della richiesta del metodo
  • out: Parametro inserito nella “busta” (envelop) della risposta del metodo
  • inout: Parametro inserito sia nella “busta” (envelop) della richiesta che nella busta della risposta del metodo. Simula la feature del by-ref da client a server.

isArray: Se impostato a true, rende la proprietà una lista della tipologia indicata nel campo “type”

optional: Se true, rende il campo opzionale

Entities

In questa sezione è possibile definire una lista di entità di dominio. Le entity sono le classi che modellano la struttura del database, e sono dunque anche gli oggetti che vengono persistiti in esso.

entities:
  - name: MyEntity
    description: My entity
    annotations: ...
    useRepository: true
    repositoryName: MyRepository
    fields: ...

name: Nome dell’entità. Il nome deve rispettare la notazione PascalCase.

description: Descrizione dell’entità.

useRepository: Se `true`, verrà generato un repository per l’entità.

repositoryName: Se specificato, imposta il nome del repository.

fields: Lista di proprietà dell’entità.

Entity Fields

La proprietà “fields” di una entity, permette di definire la lista delle proprietà che definiscono le caratteristiche di una entità.

entities:
- name: MyEntity
  ...
  fields:
  - name: myProperty
    type: string
    description: My property

name: Nome della proprietà dell’entità. Il nome deve rispettare la notazione camelCase.

description: Descrizione del field

isNullable: [DEPRECATED] – usare la proprietà isOptional

isOptional: Se impostato a true, rende il campo nullable lato C#

type: Tipologia della proprietà. Può essere scelto dalla lista dei suggerimenti o può essere identificata in una entità definita nello stesso YAML. La lista dei suggerimenti invece, presenta dei “tipi” rappresentanti una sorta di astrazione rispetto al linguaggio nel quale vengono generati. Di seguito tutti i tipi:

  • boolean: Boolean
  • byte: System.Byte
  • date: DateTime?
  • datetime: DateTime?
  • decimal: decimal
  • float: float
  • integer: Int32
  • numeric: Double
  • sequence: Int64
  • string: String
  • time: DateTime?
  • uint8array: System.Byte[]
  • uuid: System.Guid

Mappings

Questa sezione permette di definire i mappings tra i dto e le entity. Il risultato è la generazione di classi di configurazione dei mappings, utilizzando la liberia AutoMapper. Nell’esempio, viene creato il mapping dall’entità MyProperty al dto MyDto. Il mapping è unidirezionale; è possibile invertire la direzione del mapping utilizzando il campo `inverse`.

mappings:
  - entity: MyProperty
    contract: MyDto
    inverse: false
    fields:
      - from: entityFieldName
        to: dtoFieldName

entity: Il nome della entity.

contract: Il nome del dto.

inverse: Se true, inverte il mapping, definendolo dal dto alla entity.

fields: la sezione permette di mappare una specifica coppia di campi quando i loro nomi differiscono

EJS

EJS è un semplice “templating language” che consente di generare qualsiasi snippet di codice con JavaScript. Nessuna religiosità su come organizzare le cose. Nessuna reinvenzione dell’iterazione e del flusso di controllo. È solo un semplice JavaScript.

EJS è utilizzato nel platform per generare tutto il codice dei progetti. Il platform fornisce “out of the box” una libreria di template EJS che è possibile sovrascrivere in funzione delle esigenze del progetto. Di seguito un esempio di template EJS del platform:

<%# /* Context = { entity: Entity, container: RootContainer } */ %>
<%
  const rootNamespace = rootNamespaceOfContainer(container);
  const name = entity.name;
  const namespace = fullNamespaceOfContainer(container);
  const fullName = `${namespace}.${name}`;
  const keys = entityKeysSequence(entity);
  const validations = entity.validations;
  const hasValidations = !!validations && validations.length > 0;
  const isAbstract = entity.isAbstract;
  const isTrackable = entity.isTrackable;
  const resource = entity.resource;
-%>
<%- render('documentation/entity.ejs', { entity: entity }); %>
@JsonObject({ name: '<%- fullName %>, <%- rootNamespace %>' })
@Entity({
  name: '<%- fullName %>',
  keys: [<%- keys %>]
})
<% if (hasValidations) { -%>
  <%- render('model/entity/validation-collection.ejs', { validations: validations }); %>
<% } -%>
<% if (resource) { -%>
  <%- render('module/resource.ejs', { uri: resource }); %>
<% } -%>
export <%- isAbstract ? 'abstract ' : '' %>class <%- name %> extends <%- getExtendsClass(entity, container); %> {
  <%- render('model/entity/entity-body.ejs', { entity: entity, container: container }); %>
}

Per maggior dettagli sulla sintassi del linguaggio EJS, visita la documentazione ufficiale

Templates

I template EJS sono il cuore della generazione del codice. I template sono utilizzati per trasformare in codice organizzato, ingegnerizzato, compilabile ed eseguibile tutto ciò che è stato definito all’interno dei file YAML (container, entities, operations, …)

E’ possibile sovrascrivere qualsiasi template EJS utilizzando il task di Scarface: “Override Codegen template”. Scarface chiederà all’utente il percorso esatto del template da sovrascrivere. Al termine di questa procedura, il template verrà copiato nella folder “codegentemplates” del progetto e potrà essere modificato, applicando le novità introdotte al successivo ciclo di generazione.

Ricavare il template utilizzato per generare uno snippet di codice, è molto semplice: tutti gli snippet iniziano con il commento / #begin-template / e terminano con il commento / #end-template /, dove indica il percorso del template EJS di riferimento:

/* #begin-template component/state.ejs */
  ...
  /* #end-template component/state.ejs */

Tutti i file generati, sono contrassegnati con un “header” al top del file che indica come comportarsi con il file in questione. Il seguente header indica che il file NON deve essere assolutamente modificato, in quanto ad ogni nuovo ciclo di generazione verrà totalmente sovrascritto.

/**********************************************************
  * Automatically produced by CA code generator            *
  *                                                        *
  * IMPORTANT NOTE:                                        *
  *                                                        *
  * Auto generated file. Do not modify please.             *
**********************************************************/

Il seguente header invece indica che il file può essere modificato, in quanto dopo la prima generazione, non verrà più toccato:

/**********************************************************
  * Automatically produced by CA code generator            *
  *                                                        *
  * IMPORTANT NOTE:                                        *
  *                                                        *
  * Auto generated file. This file CAN be modified by you. *
**********************************************************/

Quest ultimo header infine indica che è possibile modificare il file in tutti i punti, tranne che nei “punti di iniezione”:

/**********************************************************
  * Automatically produced by CA code generator            *
  *                                                        *
  * IMPORTANT NOTE:                                        *
  *                                                        *
  * Auto generated file. This file CAN be modified by you. *
  * Do not change injection points please.                 *
**********************************************************/

contrassegnati come di seguito:

// --inject:imports--
 import { JsonObject, JsonIgnore } from '@ca-webstack/reflection';
 import { IActivityPayload, IActivityAnnotation } from '@ca-webstack/ng-shell';
 // --inject:imports--
 import { Employee } from './../../../models/index';