@redpanda-data/docs-extensions-and-macros 4.10.0 → 4.10.2

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.
@@ -12,12 +12,12 @@ logger = logging.getLogger(__name__)
12
12
  # the centralized enterprise value processing logic without creating import cycles.
13
13
  def get_process_enterprise_value():
14
14
  """
15
- Lazily import and return the centralized `process_enterprise_value` function from `property_extractor`.
15
+ Lazily load the centralized process_enterprise_value function from property_extractor.
16
16
 
17
- Attempts to import `process_enterprise_value` and return it to avoid circular-import issues. If the import fails an error message is printed and None is returned.
17
+ Attempts to import and return the `process_enterprise_value` callable; logs an error and returns `None` if the import fails.
18
18
 
19
19
  Returns:
20
- Callable or None: The `process_enterprise_value` callable when available, otherwise `None`.
20
+ The `process_enterprise_value` callable if available, `None` otherwise.
21
21
  """
22
22
  try:
23
23
  from property_extractor import process_enterprise_value
@@ -27,6 +27,23 @@ def get_process_enterprise_value():
27
27
  return None
28
28
 
29
29
 
30
+ def get_resolve_constexpr_identifier():
31
+ """
32
+ Lazily import and return the `resolve_constexpr_identifier` function from `property_extractor`.
33
+
34
+ Attempts to import `resolve_constexpr_identifier` and return it to avoid circular-import issues.
35
+
36
+ Returns:
37
+ Callable or None: The `resolve_constexpr_identifier` callable when available, otherwise `None`.
38
+ """
39
+ try:
40
+ from property_extractor import resolve_constexpr_identifier
41
+ return resolve_constexpr_identifier
42
+ except ImportError as e:
43
+ logger.exception("Cannot import resolve_constexpr_identifier from property_extractor: %s", e)
44
+ return None
45
+
46
+
30
47
  class BasicInfoTransformer:
31
48
  def accepts(self, info, file_pair):
32
49
  """
@@ -182,29 +199,59 @@ class TypeTransformer:
182
199
 
183
200
  def get_cpp_type_from_declaration(self, declaration):
184
201
  """
185
- Extract the inner type from C++ property declarations.
186
-
187
- This method handles various C++ template types and extracts the core type T from:
188
- - property<T> -> T
189
- - std::optional<T> -> T
190
- - std::vector<T> -> T
191
- - one_or_many_property<T> -> T (Redpanda's flexible array type)
202
+ Extract the inner C++ type from wrapped declarations like `property<T>`, `std::optional<T>`, `std::vector<T>`, or `one_or_many_property<T>`.
192
203
 
193
- For one_or_many_property, this is crucial because it allows the same property
194
- to accept either a single value or an array of values in the configuration.
195
- Examples:
196
- - one_or_many_property<model::broker_endpoint> -> model::broker_endpoint
197
- - one_or_many_property<endpoint_tls_config> -> endpoint_tls_config
204
+ Parses common wrapper templates and returns the unwrapped type name (for example, returns `model::broker_endpoint` from `one_or_many_property<model::broker_endpoint>`). The returned type is intended for downstream mapping to JSON schema types and default value resolution.
198
205
 
199
- The extracted type is then used to determine the JSON schema type and
200
- for resolving default values from the definitions.
206
+ Returns:
207
+ raw_type (str): The extracted inner C++ type as a string, or a best-effort fragment of the declaration if a precise extraction cannot be performed.
201
208
  """
202
209
  one_line_declaration = declaration.replace("\n", "").strip()
203
- raw_type = (
204
- re.sub(r"^.*property<(.+)>.*", "\\1", one_line_declaration)
205
- .split()[0]
206
- .replace(",", "")
207
- )
210
+
211
+ # Extract property template content with proper nesting handling
212
+ # This handles cases like property<std::vector<config::sasl_mechanisms_override>>
213
+ def extract_template_content(text, template_name):
214
+ """
215
+ Extracts the inner contents of the first occurrence of a template with the given name, correctly handling nested angle brackets.
216
+
217
+ Parameters:
218
+ text (str): The string to search for the template.
219
+ template_name (str): The template name (e.g., "std::vector" or "property").
220
+
221
+ Returns:
222
+ str or None: The substring inside the outermost angle brackets for the matched template (excluding the brackets),
223
+ or `None` if the template is not found or angle brackets are unbalanced.
224
+ """
225
+ start_idx = text.find(f'{template_name}<')
226
+ if start_idx == -1:
227
+ return None
228
+
229
+ start_idx += len(f'{template_name}<')
230
+ bracket_count = 1
231
+ i = start_idx
232
+
233
+ while i < len(text) and bracket_count > 0:
234
+ if text[i] == '<':
235
+ bracket_count += 1
236
+ elif text[i] == '>':
237
+ bracket_count -= 1
238
+ i += 1
239
+
240
+ if bracket_count == 0:
241
+ return text[start_idx:i-1]
242
+ return None
243
+
244
+ # Extract the content from property<...>
245
+ property_content = extract_template_content(one_line_declaration, 'property')
246
+ if property_content:
247
+ raw_type = property_content.split()[0].replace(",", "")
248
+ else:
249
+ # Fallback to original regex for simpler cases
250
+ raw_type = (
251
+ re.sub(r"^.*property<(.+)>.*", "\\1", one_line_declaration)
252
+ .split()[0]
253
+ .replace(",", "")
254
+ )
208
255
 
209
256
  if self.OPTIONAL_PATTERN in raw_type:
210
257
  raw_type = re.sub(".*std::optional<(.+)>.*", "\\1", raw_type)
@@ -223,6 +270,15 @@ class TypeTransformer:
223
270
  return raw_type
224
271
 
225
272
  def get_type_from_declaration(self, declaration):
273
+ """
274
+ Map a C++ type declaration string to a simplified, user-facing type name.
275
+
276
+ Parameters:
277
+ declaration (str): C++ type declaration or template expression from which the effective type will be derived.
278
+
279
+ Returns:
280
+ str: A JSON-schema-friendly type name such as "integer", "number", "string", "string[]", or "boolean". If no mapping matches, returns the normalized/raw extracted C++ type.
281
+ """
226
282
  raw_type = self.get_cpp_type_from_declaration(declaration)
227
283
  type_mapping = [ # (regex, type)
228
284
  ("^u(nsigned|int)", "integer"),
@@ -240,9 +296,29 @@ class TypeTransformer:
240
296
  if re.search(m[0], raw_type):
241
297
  return m[1]
242
298
 
299
+ # Handle specific user-unfriendly C++ types with descriptive alternatives
300
+ # Map complex C++ config types to user-friendly JSON schema types
301
+ user_friendly_types = {
302
+ "config::sasl_mechanisms_override": "object",
303
+ }
304
+
305
+ if raw_type in user_friendly_types:
306
+ return user_friendly_types[raw_type]
307
+
243
308
  return raw_type
244
309
 
245
310
  def parse(self, property, info, file_pair):
311
+ """
312
+ Set the property's "type" field to the JSON schema type derived from the C++ declaration.
313
+
314
+ Parameters:
315
+ property (dict): Mutable property bag to be updated.
316
+ info (dict): Parsed property metadata; its "declaration" field is used to determine the type.
317
+ file_pair: Unused here; present for transformer interface compatibility.
318
+
319
+ Returns:
320
+ property (dict): The same property bag with "type" set to the derived type string.
321
+ """
246
322
  property["type"] = self.get_type_from_declaration(info["declaration"])
247
323
  return property
248
324
 
@@ -394,42 +470,170 @@ class FriendlyDefaultTransformer:
394
470
 
395
471
  # Class-level constants for pattern matching in default values
396
472
  ARRAY_PATTERN_STD_VECTOR = "std::vector"
473
+ SSTRING_CONSTRUCTOR_PATTERN = r'ss::sstring\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
474
+ VECTOR_INITIALIZER_PATTERN = r'std::vector<[^>]+>\s*\{(.*)\}$'
475
+ CHRONO_PATTERN = r"std::chrono::(\w+)\(([^)]+)\)"
476
+
477
+ def __init__(self):
478
+ """
479
+ Initialize the transformer and set up a placeholder for a lazily-loaded resolver.
480
+
481
+ Sets self._resolver to None to indicate the resolver has not been loaded yet.
482
+ """
483
+ self._resolver = None
397
484
 
398
485
  def accepts(self, info, file_pair):
486
+ """
487
+ Determine whether the transformer should run for the given property info by checking for a fourth parameter.
488
+
489
+ Parameters:
490
+ info (dict): Parsed property metadata; expects a "params" list when present.
491
+ file_pair (tuple): Source/implementation file pair (unused by this check).
492
+
493
+ Returns:
494
+ `true` if `info["params"]` exists and contains at least four items, `false` otherwise.
495
+ """
399
496
  return info.get("params") and len(info["params"]) > 3
400
497
 
498
+ def _get_resolver(self):
499
+ """
500
+ Lazily load and cache the constexpr identifier resolver.
501
+
502
+ Returns:
503
+ callable or None: The resolver function if available, or `None` if it could not be loaded.
504
+ """
505
+ if self._resolver is None:
506
+ resolver = get_resolve_constexpr_identifier()
507
+ self._resolver = resolver if resolver else False
508
+ return self._resolver if self._resolver is not False else None
509
+
510
+ def _resolve_identifier(self, identifier):
511
+ """
512
+ Resolve a constexpr identifier to its corresponding string value.
513
+
514
+ Parameters:
515
+ identifier (str): Identifier to resolve (for example, "scram" or "gssapi").
516
+
517
+ Returns:
518
+ str or None: The resolved string value if successful, `None` when the identifier is invalid or cannot be resolved.
519
+ """
520
+ if not identifier or not isinstance(identifier, str):
521
+ logger.warning(f"Invalid identifier for resolution: {identifier}")
522
+ return None
523
+
524
+ resolver = self._get_resolver()
525
+ if resolver:
526
+ try:
527
+ return resolver(identifier)
528
+ except (AttributeError, TypeError, ValueError) as e:
529
+ logger.debug(f"Failed to resolve identifier '{identifier}': {e}")
530
+ except Exception as e:
531
+ logger.exception(f"Unexpected error resolving identifier '{identifier}': {e}")
532
+
533
+ return None
534
+
535
+ def _process_sstring_constructor(self, item):
536
+ """
537
+ Convert an ss::sstring{identifier} constructor string to its resolved value when possible.
538
+
539
+ If the input matches the ss::sstring{...} pattern, attempts to resolve the enclosed identifier and returns the resolved string. If resolution fails, returns the raw identifier. If the input does not match the pattern or is falsy, returns it unchanged.
540
+
541
+ Parameters:
542
+ item (str): The constructor expression or string to process.
543
+
544
+ Returns:
545
+ str: The resolved string when resolution succeeds, the extracted identifier if resolution fails, or the original input if it does not match.
546
+ """
547
+ if not item:
548
+ return item
549
+
550
+ match = re.match(self.SSTRING_CONSTRUCTOR_PATTERN, item)
551
+ if not match:
552
+ return item
553
+
554
+ identifier = match.group(1)
555
+ resolved = self._resolve_identifier(identifier)
556
+
557
+ if resolved:
558
+ logger.debug(f"Resolved ss::sstring{{{identifier}}} -> '{resolved}'")
559
+ return resolved
560
+
561
+ # Log warning but continue with original identifier
562
+ logger.warning(f"Could not resolve identifier '{identifier}' in ss::sstring constructor")
563
+ return identifier
564
+
565
+ def _parse_vector_contents(self, contents):
566
+ """
567
+ Parse a comma-separated std::vector initializer string into a list of cleaned, processed items.
568
+
569
+ Parameters:
570
+ contents (str): The inner contents of a vector initializer (e.g. '\"a\", ss::sstring{ID}, \"b\"'); may be empty or None.
571
+
572
+ Returns:
573
+ list: Ordered list of processed, unquoted items with empty entries omitted.
574
+ """
575
+ if not contents:
576
+ return []
577
+
578
+ # Split by comma and process each item
579
+ raw_items = [contents] if ',' not in contents else contents.split(',')
580
+
581
+ processed_items = []
582
+ for item in raw_items:
583
+ item = item.strip(' "\'')
584
+ if item: # Skip empty items
585
+ processed_item = self._process_sstring_constructor(item)
586
+ processed_items.append(processed_item)
587
+
588
+ return processed_items
589
+
401
590
  def parse(self, property, info, file_pair):
591
+ """
592
+ Convert a C++ default expression into a JSON-friendly value and store it on the property under the "default" key.
593
+
594
+ Parameters:
595
+ property (dict): Property dictionary to modify; updated in place with a "default" entry.
596
+ info (dict): Parsed property information; the default expression is expected at info["params"][3]["value"].
597
+ file_pair: File pair context (ignored by this function).
598
+
599
+ Returns:
600
+ dict: The modified property dictionary with a normalized "default" value.
601
+ """
402
602
  default = info["params"][3]["value"]
603
+
604
+ # Handle null/empty defaults
605
+ if not default:
606
+ return property
403
607
 
404
- # Transform std::nullopt into None.
608
+ # Transform std::nullopt into None
405
609
  if "std::nullopt" in default:
406
610
  property["default"] = None
407
611
  return property
408
612
 
409
- # Transform std::numeric_limits expressions.
613
+ # Transform std::numeric_limits expressions
410
614
  if "std::numeric_limits" in default:
411
615
  property["default"] = "Maximum value"
412
616
  return property
413
617
 
414
- # Transform std::chrono durations.
618
+ # Transform std::chrono durations
415
619
  if "std::chrono" in default:
416
- m = re.search(r"std::chrono::(\w+)\(([^)]+)\)", default)
417
- if m:
418
- unit = m.group(1)
419
- value = m.group(2).strip()
620
+ match = re.search(self.CHRONO_PATTERN, default)
621
+ if match:
622
+ unit = match.group(1)
623
+ value = match.group(2).strip()
420
624
  property["default"] = f"{value} {unit}"
421
625
  return property
422
626
 
423
- # Transform std::vector defaults.
627
+ # Transform std::vector defaults
424
628
  if self.ARRAY_PATTERN_STD_VECTOR in default:
425
- m = re.search(r'\{([^}]+)\}', default)
426
- if m:
427
- contents = m.group(1).strip()
428
- items = [item.strip(' "\'') for item in contents.split(',')]
629
+ vector_match = re.search(self.VECTOR_INITIALIZER_PATTERN, default)
630
+ if vector_match:
631
+ contents = vector_match.group(1).strip()
632
+ items = self._parse_vector_contents(contents)
429
633
  property["default"] = items
430
634
  return property
431
635
 
432
- # Otherwise, leave the default as-is.
636
+ # For all other cases, leave the default as-is
433
637
  property["default"] = default
434
638
  return property
435
639
 
@@ -566,4 +770,4 @@ class MetaParamTransformer:
566
770
  key, value = item.split('=')
567
771
  meta_dict[key.strip().replace('.', '')] = value.strip()
568
772
  meta_dict['type'] = 'initializer_list' # Enforce required type
569
- param['value'] = meta_dict
773
+ param['value'] = meta_dict