1
1
from __future__ import annotations
2
2
3
- from collections .abc import Mapping
3
+ from collections .abc import Callable , Mapping
4
4
from dataclasses import MISSING , dataclass , field , fields
5
- from typing import TYPE_CHECKING , Any , Callable , Literal , TypedDict
5
+ from typing import TYPE_CHECKING , Any , Literal , TypedDict
6
6
7
7
from docutils .parsers .rst import directives
8
8
from sphinx .application import Sphinx
11
11
from sphinx_needs .data import GraphvizStyleType , NeedsCoreFields
12
12
from sphinx_needs .defaults import DEFAULT_DIAGRAM_TEMPLATE
13
13
from sphinx_needs .logging import get_logger , log_warning
14
+ from sphinx_needs .schema .config import (
15
+ USER_CONFIG_SCHEMA_SEVERITIES ,
16
+ MessageRuleEnum ,
17
+ SchemaType ,
18
+ SeverityEnum ,
19
+ )
14
20
15
21
if TYPE_CHECKING :
16
22
from sphinx .util .logging import SphinxLoggerAdapter
23
29
LOGGER = get_logger (__name__ )
24
30
25
31
32
+ # TODO: checking any schema against the meta model
33
+ # TODO: constrain the schema to disallow certain keys
34
+
35
+
26
36
@dataclass
27
37
class ExtraOptionParams :
28
38
"""Defines a single extra option for needs"""
@@ -31,6 +41,25 @@ class ExtraOptionParams:
31
41
"""A description of the option."""
32
42
validator : Callable [[str | None ], str ]
33
43
"""A function to validate the directive option value."""
44
+ schema : dict [str , Any ] | None
45
+ """A JSON schema for the option."""
46
+
47
+
48
+ class ExtraLinkSchemaItemsType (TypedDict ):
49
+ type : Literal ["string" ]
50
+
51
+
52
+ class ExtraLinkSchemaType (TypedDict ):
53
+ """Defines a schema for a need extra link."""
54
+
55
+ type : Literal ["array" ]
56
+ """Type for extra links, can only be array."""
57
+ items : ExtraLinkSchemaItemsType
58
+ """Schema constraints for link strings."""
59
+ minItems : NotRequired [int ]
60
+ """Minimum number of items in the array."""
61
+ maxItems : NotRequired [int ]
62
+ """Maximum number of items in the array."""
34
63
35
64
36
65
class FieldDefault (TypedDict ):
@@ -91,6 +120,7 @@ def add_extra_option(
91
120
name : str ,
92
121
description : str ,
93
122
* ,
123
+ schema : dict [str , Any ] | None = None ,
94
124
validator : Callable [[str | None ], str ] | None = None ,
95
125
override : bool = False ,
96
126
) -> None :
@@ -110,7 +140,9 @@ def add_extra_option(
110
140
111
141
raise NeedsApiConfigWarning (f"Option { name } already registered." )
112
142
self ._extra_options [name ] = ExtraOptionParams (
113
- description , directives .unchanged if validator is None else validator
143
+ description ,
144
+ directives .unchanged if validator is None else validator ,
145
+ schema ,
114
146
)
115
147
116
148
@property
@@ -240,6 +272,13 @@ class LinkOptionsType(TypedDict, total=False):
240
272
"""Used for needflow. Default: '->'"""
241
273
allow_dead_links : bool
242
274
"""If True, add a 'forbidden' class to dead links"""
275
+ schema : ExtraLinkSchemaType
276
+ """
277
+ A JSON schema for the link option.
278
+
279
+ If given, the schema will apply to all needs that use this link option.
280
+ For more granular control and graph traversal, use the `needs_schemas` configuration.
281
+ """
243
282
244
283
245
284
class NeedType (TypedDict ):
@@ -263,6 +302,32 @@ class NeedExtraOption(TypedDict):
263
302
name : str
264
303
description : NotRequired [str ]
265
304
"""A description of the option."""
305
+ type : NotRequired [Literal ["string" , "integer" , "number" , "boolean" ]]
306
+ """
307
+ The data type for schema validation. The option will still be stored as a string,
308
+ but during schema validation, the value will be coerced to the given type.
309
+
310
+ The type semantics are align with JSON Schema, see
311
+ https://json-schema.org/understanding-json-schema/reference/type.
312
+
313
+ For booleans, a predefined set of truthy/falsy strings are accepted:
314
+ - truthy = {"true", "yes", "y", "on", "1"}
315
+ - falsy = {"false", "no", "n", "off", "0"}
316
+
317
+ ``null`` is not a valid value as Sphinx options cannot actively be set to ``null``.
318
+ Sphinx-Needs does not distinguish between extra options being not given and given as empty string.
319
+ Both cases are coerced to an empty string value ``''``.
320
+ """
321
+ schema : NotRequired [dict [str , Any ]]
322
+ """
323
+ A JSON schema for the option.
324
+
325
+ If given, the schema will apply to all needs that use this option.
326
+ For more granular control, use the `needs_schemas` configuration.
327
+ """
328
+ # TODO check schema on config-inited, disallow certain keys such as
329
+ # [if, then, else, dependentSchemas, dependentRequired, anyOf, oneOf, not]
330
+ # only allow those once usecases are requested
266
331
267
332
268
333
class NeedStatusesOption (TypedDict ):
@@ -365,6 +430,120 @@ def get_default(cls, name: str) -> Any:
365
430
default_factory = list , metadata = {"rebuild" : "env" , "types" : (list ,)}
366
431
)
367
432
"""Path to the root table in the toml file to load configuration from."""
433
+ schemas : list [SchemaType ] = field (
434
+ default_factory = list ,
435
+ metadata = {
436
+ "rebuild" : "env" ,
437
+ "types" : (list ,),
438
+ "schema" : {
439
+ "type" : "array" ,
440
+ "items" : {
441
+ "type" : "object" ,
442
+ "properties" : {
443
+ "id" : {"type" : "string" , "pattern" : "^[a-zA-Z0-9_-]+$" },
444
+ "severity" : {
445
+ "type" : "string" ,
446
+ "enum" : [
447
+ e .name
448
+ for e in SeverityEnum
449
+ if e in USER_CONFIG_SCHEMA_SEVERITIES
450
+ ],
451
+ },
452
+ "message" : {"type" : "string" },
453
+ "types" : {
454
+ "type" : "array" ,
455
+ "items" : {"type" : "string" , "minLength" : 1 },
456
+ },
457
+ "local_schema" : {"type" : "object" },
458
+ "trigger_schema" : {"type" : "object" },
459
+ "trigger_schema_id" : {
460
+ "type" : "string" ,
461
+ "pattern" : "^[a-zA-Z0-9_-]+$" ,
462
+ },
463
+ "link_schema" : {
464
+ "type" : "object" ,
465
+ "patternProperties" : {
466
+ "^.*$" : {
467
+ "type" : "object" ,
468
+ "properties" : {
469
+ "schema_id" : {"type" : "string" },
470
+ "minItems" : {"type" : "integer" },
471
+ "maxItems" : {"type" : "integer" },
472
+ "unevaluatedItems" : {"type" : "boolean" },
473
+ },
474
+ "additionalProperties" : False ,
475
+ }
476
+ },
477
+ "additionalProperties" : False ,
478
+ },
479
+ "dependency" : {"type" : "boolean" },
480
+ },
481
+ "additionalProperties" : False ,
482
+ },
483
+ },
484
+ },
485
+ )
486
+ schemas_from_json : str | None = field (
487
+ default = None , metadata = {"rebuild" : "env" , "types" : (str , type (None ))}
488
+ )
489
+ """Path to a JSON file to load the schemas from."""
490
+
491
+ schemas_severity : str = field (
492
+ default = SeverityEnum .info .name ,
493
+ metadata = {
494
+ "rebuild" : "env" ,
495
+ "types" : (str ,),
496
+ "schema" : {
497
+ "type" : "string" ,
498
+ "enum" : [
499
+ e .name for e in SeverityEnum if e in USER_CONFIG_SCHEMA_SEVERITIES
500
+ ],
501
+ },
502
+ },
503
+ )
504
+ """Severity level for the schema validation reporting."""
505
+
506
+ schemas_debug_active : bool = field (
507
+ default = False ,
508
+ metadata = {"rebuild" : "env" , "types" : (bool ,), "schema" : {"type" : "boolean" }},
509
+ )
510
+ """Activate the debug mode for schema validation to dump JSON/schema files and messages."""
511
+
512
+ schemas_debug_path : str = field (
513
+ default = "schema_debug" ,
514
+ metadata = {
515
+ "rebuild" : "env" ,
516
+ "types" : (str ,),
517
+ "schema" : {"type" : "string" , "minLength" : 1 },
518
+ },
519
+ )
520
+ """
521
+ Path to the directory where the debug files are stored.
522
+
523
+ If the path is relative, the caller needs to make sure
524
+ it gets converted to a use case specific absolute path, e.g.
525
+ with confdir for Sphinx.
526
+ """
527
+
528
+ schemas_debug_ignore : list [str ] = field (
529
+ default_factory = lambda : [
530
+ "skipped_dependency" ,
531
+ "skipped_wrong_type" ,
532
+ "validation_success" ,
533
+ ],
534
+ metadata = {
535
+ "rebuild" : "env" ,
536
+ "types" : (list ,),
537
+ "schema" : {
538
+ "type" : "array" ,
539
+ "items" : {
540
+ "type" : "string" ,
541
+ "enum" : [e .value for e in MessageRuleEnum ],
542
+ },
543
+ },
544
+ },
545
+ )
546
+ """List of scenarios that are ignored for dumping debug information."""
368
547
369
548
types : list [NeedType ] = field (
370
549
default_factory = lambda : [
0 commit comments