Créer un langage de config “SASS des appsettings” :
- 1 fichier source expressif
- génération sur disque de :
appsettings.jsonappsettings.<Env>.json(pour chaque environnement)
- Déterministe : pas de
now(), random, accès réseau, I/O implicite. - Build-time : compilation via MSBuild Task / tool ; pas d’injection runtime.
- Prévisible :
- objets : merge profond
- primitives : override
- listes : remplacement (par défaut)
- SASS-like :
include,let, expressions, interpolation,for,if inline
V2 inclut tout V1 + :
include "<path>"- variables
let(globales + localesenv+ scopes locaux) - expressions :
+ - * /, comparaisons, logique,?? - interpolation string
"http://${host}:${port}" foren bloc (dans les tableaux)ifinline (sur assignations ; optionnel sur éléments de tableau)- opérateur set-if-missing :
:=
if/elseen bloc+=append liste- macros/mixins
- validation avancée / schémas
- au moins un
settings { ... }global. Plusieurs blocs sont autorisés et fusionnent en profondeur dans l'ordre du document, ce qui est ce qui permet à un fichier inclus d'en contribuer un. - 0..N
env "<Name>" { ... } includeetletautorisés au global et dansenv
- base :
appsettings.json - par env :
appsettings.<Env>.jsonpour chaqueenv "<Env>"
include "./common.settex"
include "./logging.settex"Règles
- chemin relatif au fichier courant (recommandé)
- inclusion “comme si copié/collé” à cet endroit (AST concat)
- cycle d’include = erreur
let basePort = 5000
let host = "localhost"- global : visible partout
env: masque le global pour cet env- scopes locaux :
- dans un
for(corps du for) - (V2 n’a pas de
if {}bloc, donc pas de scope if-bloc)
- dans un
- un
letdoit être défini avant usage dans le même scope (pas de hoisting)
- arithmétique :
+ - * / - comparaison :
== != < <= > >= - logique :
and or not - coalesce :
a ?? b(retournebsiaestnull)
string,number(recommandé :decimalinterne),bool,null,array,object
- opérateur utilisé sur types incompatibles => erreur
- condition d’un
if inlinedoit être bool => erreur
let host = "localhost"
let port = 5001
settings {
Service.Url = "http://${host}:${port}"
}Règles
${expr}peut apparaître plusieurs fois- si
exprvautnull→ recommandé V2 : erreur (plus sûr)
settings {
Server.Port = 8080
}Syntaxe :
Path = Expr if ConditionExemple :
settings {
Logging.LogLevel.Default = "Debug" if env == "Development"
Logging.LogLevel.Default = "Information" if env != "Development"
}Sémantique
- Si
Conditionesttrue→ appliquer l’assignation - Si
false→ ne rien faire (pas de clé créée/modifiée) - Si
Conditionn’est pas bool → erreur
Note : comme on génère plusieurs sorties,
envest une variable implicite (voir §6).
Définir une valeur seulement si la clé (chemin) n’existe pas déjà.
Syntaxe :
Path := Expr
Path := Expr if Conditionsettings {
Logging.LogLevel.Default := "Information"
Logging.LogLevel.Microsoft := "Warning"
}Si plus haut (ou via include) tu as déjà :
Logging.LogLevel.Default = "Debug"alors := ne remplace pas.
Une clé est missing si :
- elle n’existe pas dans l’objet courant (pas présente)
Recommandation V2 :
nullest considéré présent (donc:=ne remplace pas unnull).
(Tu peux choisir l’inverse, mais il faut figer et tester.)
Pour que := soit utile avec le merge base/env :
- Dans
settingsbase ::=teste uniquement “déjà présent dans base en construction”. - Dans
settingsd’unenv::=teste :- “déjà présent dans l’overlay en construction”
- et “déjà présent dans la base”
→ donc:=signifie “set uniquement si la config finale n’a pas déjà cette clé”.
settings {
Server {
Host = "0.0.0.0"
Port = 8080
}
}let svc = service {
name = "auth"
port = 5001
}Les tags (
service,item,Server, etc.) servent à construire un objet ; le tag n’est pas sérialisé en JSON.
settings {
AllowedHosts = [
"localhost"
"shop.local"
]
}settings {
Services = [
service { Name = "auth" Url = "http://localhost:5001" }
service { Name = "api" Url = "http://localhost:5002" }
]
}Rappel : “blocs uniquement” ⇒
service { ... }est une valeur objet.
Pour éviter V3 trop tôt, V2 peut autoriser :
AllowedHosts = [
"localhost"
"shop.local" if env != "Production"
"shop.com" if env == "Production"
]Sémantique
- si condition false → élément non ajouté
- condition doit être bool, sinon erreur
let services = [
service { name = "auth" port = 5001 }
service { name = "api" port = 5002 }
]
settings {
Services = [
for s in services {
item {
Name = s.name
Url = "http://localhost:${s.port}"
}
}
]
}- l’expression après
indoit être unarray, sinon erreur - le corps du
forpeut produire :item { ... }(valeur objet)- et/ou des littéraux (si tu l’autorises)
- les assignations
A.B = ...directement dans unforsont interdites (ambiguës pour une liste)
- crée
AetBen tant qu’objets si absents - si
Aexiste mais n’est pas un objet → erreur - si
Bexiste mais n’est pas un objet → erreur
Lors de la génération :
- pour
appsettings.json:env = "Base"(valeur fixe) - pour
env "Development":env = "Development"
- parser fichier + includes → AST
- évaluer
letglobaux → contexte global - évaluer
settingsglobal →BaseSettings - pour chaque env
E:- créer contexte env (copie du global)
- définir
env = E - évaluer
letlocaux deenv - évaluer
settingsinterne →Overlay(E) - appliquer
Final(E) = Merge(BaseSettings, Overlay(E))
- écrire les JSON sur disque (only-if-changed)
- objet+objet : deep merge
- primitive : override
- liste : override complet (remplacement)
- mismatch : erreur
- pas de
settingsglobal - plusieurs
settingsglobaux env "<E>"sanssettingsinterne
- fichier introuvable
- cycle d’include
- variable inconnue
- opérateur incompatible (types)
if inlinecondition non-boolforsur non-array- accès
s.namealors quesn’est pas un objet - interpolation
${expr}renvoienull(si choisi comme erreur)
- type mismatch base/overlay
- assignation traverse un non-objet (ex:
Aest liste et on faitA.B = 1)
file(line,col): error <PREFIX>###: message
Parser recommandé : descente récursive + Pratt pour les expressions.
file := topStmt* EOF ;
topStmt := includeStmt
| letStmt
| settingsBlock
| envBlock
| comment
| ";" ;
includeStmt := "include" string ;
letStmt := "let" ident "=" expr ;
settingsBlock := "settings" block ;
envBlock := "env" string block ;
block := "{" blockStmt* "}" ;
blockStmt := includeStmt
| letStmt
| assignStmt
| nestedObjectBlock
| ";"
| comment ;
nestedObjectBlock := ident block ; (* objet JSON dans un objet *)
assignStmt := path assignOp expr inlineIf? ;
assignOp := "=" | ":=" ; (* set-if-missing *)
inlineIf := "if" expr ; (* condition bool *)
path := ident ("." ident)* ;
(* Expressions *)
expr := logicalOr ;
logicalOr := logicalAnd ( "or" logicalAnd )* ;
logicalAnd := coalesce ( "and" coalesce )* ;
coalesce := equality ( "??" equality )* ;
equality := comparison ( ( "==" | "!=" ) comparison )* ;
comparison := term ( ( "<" | "<=" | ">" | ">=" ) term )* ;
term := factor ( ( "+" | "-" ) factor )* ;
factor := unary ( ( "*" | "/" ) unary )* ;
unary := ( "not" | "-" ) unary | primary ;
primary := literal
| interpolatedString
| array
| taggedObjectValue
| pathRef
| "(" expr ")" ;
pathRef := ident ("." ident)* ;
taggedObjectValue := ident block ; (* valeur objet *)
array := "[" arrayElem* "]" ;
arrayElem := ( arrayValue inlineIf? | forStmt ) arraySep? ;
arrayValue := literal
| interpolatedString
| taggedObjectValue
| pathRef ;
forStmt := "for" ident "in" expr block ;
arraySep := "," | NEWLINE ;
literal := string | number | "true" | "false" | "null" ;include "./common.settex"
let basePort = 5000
let host = "localhost"
let services = [
service { name = "auth" port = basePort + 1 }
service { name = "api" port = basePort + 2 }
]
settings {
ApplicationName = "Shop"
Server {
Host = "0.0.0.0"
Port := 8080
}
Logging {
LogLevel {
Default := "Information"
Microsoft := "Warning"
}
}
Logging.LogLevel.Default = "Debug" if env == "Development"
Logging.LogLevel.Default = "Warning" if env == "Production"
AllowedHosts = [
"localhost"
"${host}" if env != "Production"
"shop.com" if env == "Production"
]
Services = [
for s in services {
item {
Name = s.name
Url = "http://${host}:${s.port}"
}
}
]
}
env "Development" {
let basePort = 4000
settings {
Server.Port = basePort
FeatureFlags.NewCheckout := true
}
}
env "Production" {
settings {
Server.Port = 80
FeatureFlags.NewCheckout := false
Services = [
for s in services {
item {
Name = s.name
Url = "https://${s.name}.mycompany.com"
}
}
]
}
}- include + cycle detection
- let global + env + scope for
- Pratt parser expressions + bool logic + ??
- interpolation
${expr}(null => erreur) -
ifinline sur assignations (+ optionnel sur array elements) -
:=set-if-missing (avec règle spéciale env: consulte base) -
forbloc dans arrays (produit items) - diagnostics ligne/col + codes
- golden tests : include + let + if inline + := + for + interpolation
- MSBuild task : write-only-if-changed