• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright (c) 2025 Northeastern University
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19import json
20import os
21from datetime import datetime, timezone
22from typing import Any, Dict, List, NamedTuple
23from uuid import uuid4
24
25
26class FieldConfig(NamedTuple):
27    """
28    A configuration tuple representing a field's metadata.
29
30    Attributes:
31        property: Internal property name used in code
32        json_key: Corresponding JSON key name for serialization
33        required: Whether this field is mandatory
34        default: Default value for the field (supports special tokens)
35        type: Expected data type of the field
36        description: Description of the field
37        example: Example value for documentation purposes
38    """
39    property: str
40    json_key: str
41    required: bool
42    default: Any
43    type: str
44    description: str
45    example: Any
46
47
48class FieldConfigManager:
49    """
50    A manager class for handling field configurations with singleton pattern support.
51
52    Features:
53    - Loads configuration from JSON files
54    - Provides bidirectional mapping between property names and JSON keys
55    - Handles special default value tokens (timestamps, UUIDs)
56    - Implements singleton pattern for configuration instances
57    """
58
59    _instances: Dict[str, 'FieldConfigManager'] = {}
60
61    def __init__(self, config_path: str):
62        """
63        Initialize a configuration manager from a JSON file.
64
65        Args:
66            config_path: Path to the JSON configuration file
67
68        Raises:
69            FileNotFoundError: If the configuration file doesn't exist
70        """
71
72        if not os.path.exists(config_path):
73            raise FileNotFoundError(f"Configuration file not found: {config_path}")
74        with open(config_path, 'r', encoding='utf-8') as f:
75            raw_data = json.load(f)
76
77        self._by_property: Dict[str, FieldConfig] = {}
78        self._by_json_key: Dict[str, FieldConfig] = {}
79
80        for item in raw_data.get("fields", []):
81            field = FieldConfig(
82                property=item["property"],
83                json_key=item["json_key"],
84                required=item.get("required", False),
85                default=item.get("default"),
86                type=item.get("type", "string"),
87                description=item.get("description", ""),
88                example=item.get("example")
89            )
90            self._by_property[field.property] = field
91            self._by_json_key[field.json_key] = field
92
93    @classmethod
94    def get_instance(cls, config_name: str = "default") -> 'FieldConfigManager':
95        """
96        Get a singleton instance of the configuration manager.
97
98        Args:
99            config_name: Name of the configuration (corresponds to filename without extension)
100
101        Returns:
102            FieldConfigManager: The singleton instance
103
104        Note:
105            Configuration files are expected to be in a 'configs' subdirectory
106            with naming pattern '{config_name}.config.json'
107        """
108        if config_name not in cls._instances:
109            config_dir = os.path.join(os.path.dirname(__file__), "configs")
110            config_path = os.path.join(config_dir, f"{config_name}.config.json")
111            cls._instances[config_name] = cls(config_path)
112        return cls._instances[config_name]
113
114    def is_required(self, property_name: str) -> bool:
115        """
116        Check if a field is required.
117
118        Args:
119            property_name: Internal property name to check
120
121        Returns:
122            bool: True if the field is required, False otherwise
123        """
124        field = self._by_property.get(property_name)
125        return field.required if field else False
126
127    def get_default(self, property_name: str) -> Any:
128        """
129        Get the default value for a field with special token handling.
130
131        Supported special tokens in default values:
132        - "{now_utc_iso}": Replaced with current UTC time in ISO 8601 format (Z suffix)
133        - "{uuid}": Replaced with a new UUID4 string
134        - Lists/Dictionaries: Returns a deep copy to prevent reference sharing
135
136        Args:
137            property_name: Internal property name to lookup
138
139        Returns:
140            Any: The processed default value or None if not found/specified
141        """
142        field = self._by_property.get(property_name)
143        if not field:
144            return None
145
146        default = field.default
147        if default is None:
148            return None
149
150        if isinstance(default, str):
151            if default == "{now_utc_iso}":
152                return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
153            elif "{uuid}" in default:
154                return default.format(uuid=str(uuid4()))
155        elif isinstance(default, list):
156            return [item.copy() if isinstance(item, (dict, list)) else item for item in default]
157        elif isinstance(default, dict):
158            return default.copy()
159
160        return default
161
162    def all_properties(self) -> List[str]:
163        """
164        Get all available property names.
165
166        Returns:
167            List[str]: List of all internal property names
168        """
169        return list(self._by_property.keys())
170