ndsdk-cli 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ndsdk_cli-1.0.0/MANIFEST.in +6 -0
- ndsdk_cli-1.0.0/PKG-INFO +11 -0
- ndsdk_cli-1.0.0/ndsdk/__init__.py +3 -0
- ndsdk_cli-1.0.0/ndsdk/config_normalizer.py +306 -0
- ndsdk_cli-1.0.0/ndsdk/generators/__init__.py +8 -0
- ndsdk_cli-1.0.0/ndsdk/generators/base.py +93 -0
- ndsdk_cli-1.0.0/ndsdk/generators/engine.py +715 -0
- ndsdk_cli-1.0.0/ndsdk/main.py +536 -0
- ndsdk_cli-1.0.0/ndsdk/naming.py +112 -0
- ndsdk_cli-1.0.0/ndsdk/packages/client_provider/Program.fragment.cs.j2 +1 -0
- ndsdk_cli-1.0.0/ndsdk/packages/client_provider/appsettings.fragment.json.j2 +14 -0
- ndsdk_cli-1.0.0/ndsdk/packages/client_provider/package.yaml +13 -0
- ndsdk_cli-1.0.0/ndsdk/packages/db_provider/Program.fragment.cs.j2 +3 -0
- ndsdk_cli-1.0.0/ndsdk/packages/db_provider/appsettings.fragment.json.j2 +30 -0
- ndsdk_cli-1.0.0/ndsdk/packages/db_provider/package.yaml +123 -0
- ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/Program.app.fragment.cs.j2 +1 -0
- ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/Program.fragment.cs.j2 +1 -0
- ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/package.yaml +7 -0
- ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/Program.fragment.cs.j2 +1 -0
- ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/appsettings.fragment.json.j2 +6 -0
- ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/package.yaml +9 -0
- ndsdk_cli-1.0.0/ndsdk/packages/observability/Program.fragment.cs.j2 +2 -0
- ndsdk_cli-1.0.0/ndsdk/packages/observability/appsettings.fragment.json.j2 +39 -0
- ndsdk_cli-1.0.0/ndsdk/packages/observability/package.yaml +16 -0
- ndsdk_cli-1.0.0/ndsdk/registry.py +192 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/BO.cs.j2 +10 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Controller.cs.j2 +131 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/DbProviderBase.cs.j2 +26 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Dockerfile.j2 +15 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Dto.cs.j2 +16 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Entity.cs.j2 +16 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/IService.cs.j2 +25 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Model.cs.j2 +16 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Provider.cs.j2 +151 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/ProviderBase.cs.j2 +10 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Service.cs.j2 +49 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/ServiceManager.cs.j2 +28 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/Solution.sln.j2 +22 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/_csproj_refs.inc.j2 +14 -0
- ndsdk_cli-1.0.0/ndsdk/templates/_common/launchSettings.json.j2 +17 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/Api.csproj.j2 +12 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/ClassLib.csproj.j2 +11 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/Program.cs.j2 +35 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/layout.yaml +64 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/Program.cs.j2 +61 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/Project.csproj.j2 +12 -0
- ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/layout.yaml +41 -0
- ndsdk_cli-1.0.0/ndsdk/validators.py +213 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/PKG-INFO +11 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/SOURCES.txt +54 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/dependency_links.txt +1 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/entry_points.txt +2 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/requires.txt +3 -0
- ndsdk_cli-1.0.0/ndsdk_cli.egg-info/top_level.txt +1 -0
- ndsdk_cli-1.0.0/pyproject.toml +35 -0
- ndsdk_cli-1.0.0/setup.cfg +4 -0
ndsdk_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ndsdk-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ND-SDK metadata-driven Template generator
|
|
5
|
+
Author-email: Hari Hara Sudhan P <harihara.sudhan@novacisinnovations.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Keywords: code-generation,csharp,microservice,scaffolding,yaml
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Requires-Dist: Jinja2>=3.1
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Compatibility normalisation for ND-SDK config files.
|
|
2
|
+
|
|
3
|
+
The generator internally works with one canonical provider shape. Public config
|
|
4
|
+
should describe every external capability the same way:
|
|
5
|
+
|
|
6
|
+
* ``integrations`` describe external resources (database, storage, queue, API, ...)
|
|
7
|
+
* ``providers`` describe generated adapter classes
|
|
8
|
+
* ``providers[].methods`` describe provider operations using a common method shape
|
|
9
|
+
* ``packages`` describe capabilities that must be injected into the generated app
|
|
10
|
+
|
|
11
|
+
Older database-first keys are still accepted for compatibility, but are folded
|
|
12
|
+
into the same common shape instead of creating a separate public database model.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_RELATIONAL_PROVIDERS = {
|
|
22
|
+
"sqlserver",
|
|
23
|
+
"sql_server",
|
|
24
|
+
"mssql",
|
|
25
|
+
"postgres",
|
|
26
|
+
"postgresql",
|
|
27
|
+
"mysql",
|
|
28
|
+
"oracle",
|
|
29
|
+
"snowflake",
|
|
30
|
+
}
|
|
31
|
+
_NON_RELATIONAL_PROVIDERS = {
|
|
32
|
+
"cassandra",
|
|
33
|
+
"dynamodb",
|
|
34
|
+
"mongodb",
|
|
35
|
+
"cosmosdb",
|
|
36
|
+
"cosmos",
|
|
37
|
+
"redis",
|
|
38
|
+
"elasticsearch",
|
|
39
|
+
}
|
|
40
|
+
_DB_METHOD_KEYS = {
|
|
41
|
+
"stored_procedure",
|
|
42
|
+
"target",
|
|
43
|
+
"table",
|
|
44
|
+
"inputs",
|
|
45
|
+
"parameters",
|
|
46
|
+
"cursors",
|
|
47
|
+
}
|
|
48
|
+
_METHOD_ALIAS_KEYS = {"database", "entity"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_config(config: dict[str, Any] | None) -> dict[str, Any]:
|
|
52
|
+
"""Return a deep-copied canonical config.
|
|
53
|
+
|
|
54
|
+
Supported compatibility aliases:
|
|
55
|
+
* ``defaults.observability`` -> ``defaults.packages.observability``
|
|
56
|
+
* old ``framework_packages`` blocks -> public ``packages`` blocks
|
|
57
|
+
* old ``service.database`` -> synthetic database integration only when no
|
|
58
|
+
equivalent integration exists
|
|
59
|
+
* ``endpoints[].request_dto`` / ``response_dto`` are mirrored into the older
|
|
60
|
+
request/response model fields only when those fields are absent
|
|
61
|
+
* ``providers[].methods[].entity`` -> ``input_entity``
|
|
62
|
+
* ``providers[].methods[].database`` and top-level DB method fields ->
|
|
63
|
+
``providers[].methods[].settings``
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if not isinstance(config, dict):
|
|
67
|
+
return {}
|
|
68
|
+
out = copy.deepcopy(config or {})
|
|
69
|
+
_normalize_defaults(out)
|
|
70
|
+
for svc in out.get("services", []) or []:
|
|
71
|
+
_normalize_service(svc)
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_defaults(config: dict[str, Any]) -> None:
|
|
76
|
+
defaults = config.setdefault("defaults", {})
|
|
77
|
+
|
|
78
|
+
packages = _merge_package_blocks(defaults.get("framework_packages"), defaults.get("packages"))
|
|
79
|
+
defaults.pop("framework_packages", None)
|
|
80
|
+
defaults["packages"] = packages
|
|
81
|
+
|
|
82
|
+
observability = defaults.get("observability")
|
|
83
|
+
if isinstance(observability, dict):
|
|
84
|
+
current = packages.get("observability")
|
|
85
|
+
if isinstance(current, dict):
|
|
86
|
+
packages["observability"] = {**observability, **current}
|
|
87
|
+
else:
|
|
88
|
+
packages["observability"] = {"enabled": observability.get("enabled", True), **observability}
|
|
89
|
+
elif observability is True:
|
|
90
|
+
packages.setdefault("observability", {"enabled": True})
|
|
91
|
+
elif observability is False:
|
|
92
|
+
packages.setdefault("observability", {"enabled": False})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _merge_package_blocks(*blocks: Any) -> dict[str, Any]:
|
|
96
|
+
"""Normalize package declarations to one ``{key: settings}`` map.
|
|
97
|
+
|
|
98
|
+
Supported public shapes:
|
|
99
|
+
|
|
100
|
+
``packages: { observability: { enabled: true } }``
|
|
101
|
+
``packages: [ { name: observability, enabled: true } ]``
|
|
102
|
+
``packages: [ observability ]``
|
|
103
|
+
"""
|
|
104
|
+
merged: dict[str, Any] = {}
|
|
105
|
+
for block in blocks:
|
|
106
|
+
if not block:
|
|
107
|
+
continue
|
|
108
|
+
if isinstance(block, dict):
|
|
109
|
+
merged.update(block)
|
|
110
|
+
continue
|
|
111
|
+
if isinstance(block, list):
|
|
112
|
+
for item in block:
|
|
113
|
+
if isinstance(item, str):
|
|
114
|
+
merged[item] = {"enabled": True}
|
|
115
|
+
elif isinstance(item, dict):
|
|
116
|
+
key = item.get("name") or item.get("key") or item.get("package")
|
|
117
|
+
if key:
|
|
118
|
+
settings = {k: v for k, v in item.items() if k not in {"name", "key", "package"}}
|
|
119
|
+
merged[str(key)] = settings or {"enabled": True}
|
|
120
|
+
return merged
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _normalize_service(svc: dict[str, Any]) -> None:
|
|
124
|
+
svc["packages"] = _merge_package_blocks(svc.get("framework_packages"), svc.get("packages"))
|
|
125
|
+
svc.pop("framework_packages", None)
|
|
126
|
+
|
|
127
|
+
_normalize_integrations(svc)
|
|
128
|
+
_normalize_legacy_database_block(svc)
|
|
129
|
+
|
|
130
|
+
integrations = svc.get("integrations", []) or []
|
|
131
|
+
integration_by_name = {
|
|
132
|
+
i.get("name"): i for i in integrations if isinstance(i, dict) and i.get("name")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for ep in svc.get("endpoints", []) or []:
|
|
136
|
+
if "request_dto" in ep and "request_model" not in ep:
|
|
137
|
+
ep["request_model"] = ep["request_dto"]
|
|
138
|
+
if "response_dto" in ep and "response_model" not in ep:
|
|
139
|
+
ep["response_model"] = ep["response_dto"]
|
|
140
|
+
ep.setdefault(
|
|
141
|
+
"consumes",
|
|
142
|
+
"application/json" if str(ep.get("http_method", "GET")).upper() in {"POST", "PUT", "PATCH"} else "",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
for prov in svc.get("providers", []) or []:
|
|
146
|
+
integration_name = prov.get("integration")
|
|
147
|
+
integration = integration_by_name.get(integration_name)
|
|
148
|
+
provider_kind = _provider_kind(prov, integration)
|
|
149
|
+
prov["kind"] = provider_kind
|
|
150
|
+
|
|
151
|
+
# Backward compatibility: old `db: true` providers become database kind.
|
|
152
|
+
if prov.get("db") is True:
|
|
153
|
+
prov["kind"] = "database"
|
|
154
|
+
provider_kind = "database"
|
|
155
|
+
prov.pop("db", None)
|
|
156
|
+
|
|
157
|
+
if not prov.get("integration") and provider_kind == "database":
|
|
158
|
+
db_integration = next(
|
|
159
|
+
(i for i in integrations if isinstance(i, dict) and str(i.get("kind", "")).lower() == "database"),
|
|
160
|
+
None,
|
|
161
|
+
)
|
|
162
|
+
if db_integration and db_integration.get("name"):
|
|
163
|
+
prov["integration"] = db_integration["name"]
|
|
164
|
+
integration_name = prov["integration"]
|
|
165
|
+
integration = db_integration
|
|
166
|
+
|
|
167
|
+
for method in prov.get("methods", []) or []:
|
|
168
|
+
_normalize_provider_method(method, provider_kind, integration_name)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _normalize_integrations(svc: dict[str, Any]) -> None:
|
|
172
|
+
integrations = svc.get("integrations")
|
|
173
|
+
if not isinstance(integrations, list):
|
|
174
|
+
integrations = []
|
|
175
|
+
svc["integrations"] = integrations
|
|
176
|
+
|
|
177
|
+
for integration in integrations:
|
|
178
|
+
if not isinstance(integration, dict):
|
|
179
|
+
continue
|
|
180
|
+
integration["kind"] = str(integration.get("kind") or "custom").lower()
|
|
181
|
+
settings = integration.get("settings") if isinstance(integration.get("settings"), dict) else {}
|
|
182
|
+
|
|
183
|
+
# Keep all provider-specific details under the common `settings` object.
|
|
184
|
+
for key in ("database_type", "connection_string_name", "container_name", "queue_name", "base_url_config_key"):
|
|
185
|
+
if key in integration and key not in settings:
|
|
186
|
+
settings[key] = integration.pop(key)
|
|
187
|
+
if "type" in integration and integration["kind"] == "database" and "database_type" not in settings:
|
|
188
|
+
settings["database_type"] = integration.pop("type")
|
|
189
|
+
|
|
190
|
+
provider = integration.get("provider") or integration.get("store")
|
|
191
|
+
if provider and "provider" not in integration:
|
|
192
|
+
integration["provider"] = provider
|
|
193
|
+
integration.pop("store", None)
|
|
194
|
+
|
|
195
|
+
if integration["kind"] == "database" and "database_type" not in settings:
|
|
196
|
+
settings["database_type"] = _infer_db_type(str(provider or ""))
|
|
197
|
+
|
|
198
|
+
integration["settings"] = settings
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _normalize_legacy_database_block(svc: dict[str, Any]) -> None:
|
|
202
|
+
"""Fold an old service.database block into a database integration.
|
|
203
|
+
|
|
204
|
+
The normalized public copy should use integrations/providers/settings. The
|
|
205
|
+
legacy block is removed after an equivalent integration is present.
|
|
206
|
+
"""
|
|
207
|
+
db = svc.get("database")
|
|
208
|
+
if not isinstance(db, dict):
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
integrations = svc.setdefault("integrations", [])
|
|
212
|
+
has_database_integration = any(
|
|
213
|
+
isinstance(i, dict) and str(i.get("kind", "")).lower() == "database"
|
|
214
|
+
for i in integrations
|
|
215
|
+
)
|
|
216
|
+
if not has_database_integration:
|
|
217
|
+
provider = db.get("provider") or db.get("store")
|
|
218
|
+
integrations.append({
|
|
219
|
+
"name": db.get("integration") or "database",
|
|
220
|
+
"kind": "database",
|
|
221
|
+
"provider": provider,
|
|
222
|
+
"settings": {
|
|
223
|
+
"database_type": db.get("type") or _infer_db_type(str(provider or "")),
|
|
224
|
+
**({"connection_string_name": db["connection_string_name"]} if db.get("connection_string_name") else {}),
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
svc.pop("database", None)
|
|
228
|
+
_normalize_integrations(svc)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _provider_kind(prov: dict[str, Any], integration: dict[str, Any] | None) -> str:
|
|
232
|
+
if prov.get("kind"):
|
|
233
|
+
return str(prov["kind"]).lower()
|
|
234
|
+
if integration and integration.get("kind"):
|
|
235
|
+
return str(integration["kind"]).lower()
|
|
236
|
+
if prov.get("db") is True or isinstance(prov.get("database"), dict):
|
|
237
|
+
return "database"
|
|
238
|
+
|
|
239
|
+
# Compatibility: older configs expressed database providers by putting
|
|
240
|
+
# database-shaped method fields directly on provider methods. Treat that as
|
|
241
|
+
# database, then normalize those fields into method.settings.
|
|
242
|
+
for method in prov.get("methods", []) or []:
|
|
243
|
+
if not isinstance(method, dict):
|
|
244
|
+
continue
|
|
245
|
+
if isinstance(method.get("database"), dict):
|
|
246
|
+
return "database"
|
|
247
|
+
if method.get("kind") in {"relational", "non_relational"}:
|
|
248
|
+
return "database"
|
|
249
|
+
if any(key in method for key in _DB_METHOD_KEYS):
|
|
250
|
+
return "database"
|
|
251
|
+
return "custom"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _normalize_provider_method(method: dict[str, Any], provider_kind: str, integration_name: str | None) -> None:
|
|
255
|
+
if method.get("entity") and not method.get("input_entity"):
|
|
256
|
+
method["input_entity"] = method["entity"]
|
|
257
|
+
method.pop("entity", None)
|
|
258
|
+
|
|
259
|
+
settings: dict[str, Any] = {}
|
|
260
|
+
if isinstance(method.get("settings"), dict):
|
|
261
|
+
settings.update(method["settings"])
|
|
262
|
+
|
|
263
|
+
# Old database method block is now just provider-specific method settings.
|
|
264
|
+
db_block = method.get("database") if isinstance(method.get("database"), dict) else {}
|
|
265
|
+
settings = {**db_block, **settings}
|
|
266
|
+
method.pop("database", None)
|
|
267
|
+
|
|
268
|
+
for key in list(_DB_METHOD_KEYS):
|
|
269
|
+
if key in method and key not in settings:
|
|
270
|
+
settings[key] = method.pop(key)
|
|
271
|
+
|
|
272
|
+
op = str(method.get("operation", "") or "").strip()
|
|
273
|
+
if op:
|
|
274
|
+
method["operation"] = _normalize_operation(op)
|
|
275
|
+
elif provider_kind == "database":
|
|
276
|
+
if settings.get("stored_procedure"):
|
|
277
|
+
method["operation"] = "stored_procedure"
|
|
278
|
+
else:
|
|
279
|
+
method["operation"] = "query"
|
|
280
|
+
|
|
281
|
+
if provider_kind == "database" and method.get("kind") in {"relational", "non_relational"}:
|
|
282
|
+
settings.setdefault("method_type", method.pop("kind"))
|
|
283
|
+
|
|
284
|
+
method["settings"] = settings
|
|
285
|
+
method.setdefault("integration", integration_name)
|
|
286
|
+
|
|
287
|
+
for alias in _METHOD_ALIAS_KEYS:
|
|
288
|
+
method.pop(alias, None)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _normalize_operation(op: str) -> str:
|
|
292
|
+
normalized = op.strip().replace(" ", "_").replace("-", "_").lower()
|
|
293
|
+
if normalized in {"storedprocedure", "stored_proc", "stored_procedure", "sp"}:
|
|
294
|
+
return "stored_procedure"
|
|
295
|
+
if normalized in {"query", "getresults", "get_results"}:
|
|
296
|
+
return "query"
|
|
297
|
+
return normalized
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _infer_db_type(provider: str) -> str:
|
|
301
|
+
p = provider.strip().lower()
|
|
302
|
+
if p in _RELATIONAL_PROVIDERS:
|
|
303
|
+
return "relational"
|
|
304
|
+
if p in _NON_RELATIONAL_PROVIDERS:
|
|
305
|
+
return "non_relational"
|
|
306
|
+
return "non_relational"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Generators package — manifest-driven engine + variant discovery."""
|
|
2
|
+
|
|
3
|
+
from .engine import available_variants, get_generator, resolve_variant # noqa: F401
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def available_architectures() -> list[str]:
|
|
7
|
+
"""Back-compat: 'type/style' strings."""
|
|
8
|
+
return [f"{t}/{s}" for t, s in available_variants()]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Low-level generator helpers: Jinja env, file IO, namespace + nuget resolution.
|
|
2
|
+
|
|
3
|
+
Shared by the manifest engine. No architecture-specific logic lives here.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, StrictUndefined
|
|
12
|
+
|
|
13
|
+
from .. import naming
|
|
14
|
+
|
|
15
|
+
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
|
|
16
|
+
COMMON_DIR = TEMPLATES_DIR / "_common"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_env(variant_dir: Path) -> Environment:
|
|
20
|
+
"""Jinja env that searches the variant folder first, then _common."""
|
|
21
|
+
env = Environment(
|
|
22
|
+
loader=ChoiceLoader([
|
|
23
|
+
FileSystemLoader(str(variant_dir)),
|
|
24
|
+
FileSystemLoader(str(COMMON_DIR)),
|
|
25
|
+
]),
|
|
26
|
+
trim_blocks=True,
|
|
27
|
+
lstrip_blocks=True,
|
|
28
|
+
keep_trailing_newline=True,
|
|
29
|
+
undefined=StrictUndefined,
|
|
30
|
+
)
|
|
31
|
+
naming.register_filters(env)
|
|
32
|
+
return env
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_file(path: Path, content: str, *, owner: str = "framework") -> tuple[Path, bool]:
|
|
36
|
+
"""Write a file. Returns (path, written).
|
|
37
|
+
|
|
38
|
+
``owner='user'`` files are written once and never overwritten, so business
|
|
39
|
+
logic survives regeneration after a framework/controller change.
|
|
40
|
+
"""
|
|
41
|
+
if owner == "user" and path.exists():
|
|
42
|
+
return path, False
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
if not content.endswith("\n"):
|
|
45
|
+
content += "\n"
|
|
46
|
+
path.write_text(content, encoding="utf-8")
|
|
47
|
+
return path, True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_namespaces(ns_templates: dict[str, str], base_ns: str) -> dict[str, str]:
|
|
51
|
+
"""Resolve a layout's namespace map (e.g. ``{ns}.Domain.Entities``) against a
|
|
52
|
+
service's base namespace."""
|
|
53
|
+
out = {"base": base_ns}
|
|
54
|
+
for key, tmpl in (ns_templates or {}).items():
|
|
55
|
+
out[key] = tmpl.format(ns=base_ns)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_nuget_refs(
|
|
60
|
+
package_refs: list,
|
|
61
|
+
*,
|
|
62
|
+
config_versions: dict[str, str],
|
|
63
|
+
version_strategy: str = "floating",
|
|
64
|
+
) -> list[dict]:
|
|
65
|
+
"""De-duplicate NuGet refs and apply version precedence.
|
|
66
|
+
|
|
67
|
+
Precedence (highest first):
|
|
68
|
+
1. config ``package_versions[name]`` override
|
|
69
|
+
2. explicit/default version from package or layout YAML
|
|
70
|
+
3. ``nuget_version_strategy`` fallback
|
|
71
|
+
|
|
72
|
+
This keeps package YAML versions as shipped defaults, not hardcoded locks.
|
|
73
|
+
"""
|
|
74
|
+
resolved: dict[str, str] = {}
|
|
75
|
+
for ref in package_refs:
|
|
76
|
+
if isinstance(ref, str):
|
|
77
|
+
name, version = (ref.split("@", 1) + [""])[:2]
|
|
78
|
+
name, version = name.strip(), version.strip()
|
|
79
|
+
else:
|
|
80
|
+
name = str(ref.get("name"))
|
|
81
|
+
version = str(ref.get("version", "") or "")
|
|
82
|
+
if name in config_versions:
|
|
83
|
+
version = str(config_versions[name])
|
|
84
|
+
# later refs override earlier (service overrides defaults), while the
|
|
85
|
+
# config-level package_versions override remains authoritative.
|
|
86
|
+
resolved[name] = version
|
|
87
|
+
|
|
88
|
+
out = []
|
|
89
|
+
for name, version in resolved.items():
|
|
90
|
+
if not version:
|
|
91
|
+
version = "*" if version_strategy == "floating" else None
|
|
92
|
+
out.append({"name": name, "version": version})
|
|
93
|
+
return out
|