فهرست منبع

refactor: Simplify to dotted notation only, remove dict variable handling

- Removed all dict variable handling code
- Simplified TemplateVariable dataclass (no more is_dict, dict_keys)
- Simplified template parser to only handle dotted notation
- Removed _prompt_dict_variable from prompt handler
- Updated nginx template to use underscores instead of dots/dicts

This greatly simplifies the system but requires templates to use
underscores (service_port_http) instead of dots or dict access
to avoid Jinja2's attribute resolution confusion.
xcad 6 ماه پیش
والد
کامیت
b014b23f13
4فایلهای تغییر یافته به همراه22 افزوده شده و 151 حذف شده
  1. 5 54
      cli/core/prompt.py
  2. 9 78
      cli/core/template.py
  3. 4 15
      cli/core/variables.py
  4. 4 4
      library/compose/nginx/compose.yaml

+ 5 - 54
cli/core/prompt.py

@@ -118,11 +118,7 @@ class SimplifiedPromptHandler:
         console.print(f"\n[bold cyan]{display_name} - Required Configuration[/bold cyan]")
       for var_name in required:
         var = self.variables[var_name]
-        if var.is_dict:
-          console.print(f"\n[cyan]{var.name}[/cyan]")
-          self.values[var_name] = self._prompt_dict_variable(var)
-        else:
-          self.values[var_name] = self._prompt_variable(var, required=True)
+        self.values[var_name] = self._prompt_variable(var, required=True)
     
     # Process optional variables
     if optional:
@@ -135,49 +131,9 @@ class SimplifiedPromptHandler:
       if Confirm.ask("Do you want to change any values?", default=False):
         for var_name in optional:
           var = self.variables[var_name]
-          if var.is_dict:
-            console.print(f"\n[cyan]{var.name}[/cyan]")
-            self.values[var_name] = self._prompt_dict_variable(
-              var, current_values=self.values.get(var_name, {})
-            )
-          else:
-            self.values[var_name] = self._prompt_variable(
-              var, current_value=self.values[var_name]
-            )
-  
-  
-  def _prompt_dict_variable(self, var: TemplateVariable, current_values: Dict[str, Any] = None) -> Dict[str, Any]:
-    """Prompt for a dict variable with dynamic keys."""
-    result = {}
-    current_values = current_values or {}
-    
-    for key in var.dict_keys:
-      # Use current value if available, otherwise check for default
-      current_value = current_values.get(key)
-      if current_value is None and isinstance(var.default, dict):
-        current_value = var.default.get(key)
-      
-      prompt_msg = f"Enter {var.name}['{key}']"
-      if current_value is not None:
-        prompt_msg += f" [dim]({current_value})[/dim]"
-      else:
-        prompt_msg += " [red](Required)[/red]"
-      
-      # Infer type from current value or default
-      if isinstance(current_value, int):
-        while True:
-          try:
-            result[key] = IntPrompt.ask(prompt_msg, default=current_value)
-            break
-          except ValueError:
-            console.print("[red]Please enter a valid integer[/red]")
-      elif isinstance(current_value, bool):
-        result[key] = Confirm.ask(prompt_msg, default=current_value)
-      else:
-        value = Prompt.ask(prompt_msg, default=str(current_value) if current_value else None)
-        result[key] = value if value else current_value
-    
-    return result
+          self.values[var_name] = self._prompt_variable(
+            var, current_value=self.values[var_name]
+          )
   
   def _show_variables(self, var_names: List[str]):
     """Display current variable values."""
@@ -185,12 +141,7 @@ class SimplifiedPromptHandler:
       var = self.variables[var_name]
       value = self.values.get(var_name, var.default)
       if value is not None:
-        # Special formatting for dict values - show each key separately
-        if isinstance(value, dict):
-          for key, val in value.items():
-            console.print(f"  {var.display_name}['{key}']: [dim]{val}[/dim]")
-        else:
-          console.print(f"  {var.display_name}: [dim]{value}[/dim]")
+        console.print(f"  {var.display_name}: [dim]{value}[/dim]")
   
   def _prompt_variable(
     self, 

+ 9 - 78
cli/core/template.py

@@ -36,7 +36,6 @@ class Template:
   # Template variable analysis results
   vars: Set[str] = field(default_factory=set, init=False)
   var_defaults: Dict[str, Any] = field(default_factory=dict, init=False)
-  var_dict_keys: Dict[str, List[str]] = field(default_factory=dict, init=False)  # Track dict access patterns
   variables: Dict[str, TemplateVariable] = field(default_factory=dict, init=False)  # Analyzed variables
   
   def __post_init__(self):
@@ -52,10 +51,10 @@ class Template:
     self.size = self.file_path.stat().st_size if self.file_path.exists() else 0
     
     # Parse template variables
-    self.vars, self.var_defaults, self.var_dict_keys = self._parse_template_variables(self.content)
+    self.vars, self.var_defaults = self._parse_template_variables(self.content)
     # Analyze variables to create TemplateVariable objects
     self.variables = analyze_template_variables(
-      self.vars, self.var_defaults, self.var_dict_keys, self.content
+      self.vars, self.var_defaults, self.content
     )
   
   @staticmethod
@@ -96,17 +95,15 @@ class Template:
       post = frontmatter.load(f)
     return post.metadata, post.content
   
-  def _parse_template_variables(self, template_content: str) -> Tuple[Set[str], Dict[str, Any], Dict[str, List[str]]]:
+  def _parse_template_variables(self, template_content: str) -> Tuple[Set[str], Dict[str, Any]]:
     """Parse Jinja2 template to extract variables and their defaults.
     
-    Handles multiple patterns:
+    Handles:
     - Simple variables: service_name
-    - Dotted notation: traefik.host
-    - Dict notation: service_port['http']
-    - Nested patterns: nginx_dashboard.port['dashboard']
+    - Dotted notation: traefik.host, service_port.http
     
     Returns:
-        Tuple of (all_variable_names, variable_defaults, dict_access_patterns)
+        Tuple of (all_variable_names, variable_defaults)
     """
     try:
       env = self._create_jinja_env()
@@ -115,42 +112,7 @@ class Template:
       # Start with variables found by Jinja2's meta utility
       all_variables = meta.find_undeclared_variables(ast)
       
-      # Track dict access patterns
-      dict_keys = {}  # var_name -> list of keys accessed
-      
-      # Handle all Getitem nodes (dict access)
-      for node in ast.find_all(nodes.Getitem):
-        if isinstance(node.arg, nodes.Const) and isinstance(node.arg.value, str):
-          key = node.arg.value
-          current = node.node
-          
-          # Handle nested patterns like nginx_dashboard.port['dashboard']
-          if isinstance(current, nodes.Getattr):
-            # Build the full dotted name
-            parts = [current.attr]
-            base = current.node
-            while isinstance(base, nodes.Getattr):
-              parts.insert(0, base.attr)
-              base = base.node
-            if isinstance(base, nodes.Name):
-              parts.insert(0, base.name)
-              var_name = '.'.join(parts)
-              # This is a dotted variable with dict access
-              all_variables.add(var_name)
-              if var_name not in dict_keys:
-                dict_keys[var_name] = []
-              if key not in dict_keys[var_name]:
-                dict_keys[var_name].append(key)
-          
-          # Handle simple dict access like service_port['http']
-          elif isinstance(current, nodes.Name):
-            var_name = current.name
-            if var_name not in dict_keys:
-              dict_keys[var_name] = []
-            if key not in dict_keys[var_name]:
-              dict_keys[var_name].append(key)
-      
-      # Handle dotted notation variables (like traefik.host, swarm.replicas)
+      # Handle dotted notation variables (like traefik.host, service_port.http)
       for node in ast.find_all(nodes.Getattr):
         current = node.node
         # Build the full dotted name
@@ -171,37 +133,6 @@ class Template:
           if isinstance(node.node, nodes.Name):
             defaults[node.node.name] = node.args[0].value
           
-          # Handle dict access defaults
-          elif isinstance(node.node, nodes.Getitem):
-            if isinstance(node.node.arg, nodes.Const):
-              key = node.node.arg.value
-              
-              # Handle nested pattern defaults: {{ nginx_dashboard.port['dashboard'] | default(8081) }}
-              if isinstance(node.node.node, nodes.Getattr):
-                # Build the full dotted name
-                parts = []
-                current = node.node.node
-                while isinstance(current, nodes.Getattr):
-                  parts.insert(0, current.attr)
-                  current = current.node
-                if isinstance(current, nodes.Name):
-                  parts.insert(0, current.name)
-                  var_name = '.'.join(parts)
-                  if var_name not in defaults:
-                    defaults[var_name] = {}
-                  if not isinstance(defaults[var_name], dict):
-                    defaults[var_name] = {}
-                  defaults[var_name][key] = node.args[0].value
-              
-              # Handle simple dict defaults: {{ var['key'] | default(value) }}
-              elif isinstance(node.node.node, nodes.Name):
-                var_name = node.node.node.name
-                if var_name not in defaults:
-                  defaults[var_name] = {}
-                if not isinstance(defaults[var_name], dict):
-                  defaults[var_name] = {}
-                defaults[var_name][key] = node.args[0].value
-          
           # Handle dotted variable defaults: {{ traefik.host | default('example.com') }}
           elif isinstance(node.node, nodes.Getattr):
             # Build the full dotted name
@@ -215,10 +146,10 @@ class Template:
               var_name = '.'.join(parts)
               defaults[var_name] = node.args[0].value
       
-      return all_variables, defaults, dict_keys
+      return all_variables, defaults
     except Exception as e:
       logging.getLogger('boilerplates').debug(f"Error parsing template variables: {e}")
-      return set(), {}, {}
+      return set(), {}
 
   def validate(self) -> List[str]:
     """Validate template integrity.

+ 4 - 15
cli/core/variables.py

@@ -8,8 +8,7 @@ class TemplateVariable:
   
   Represents a variable found in a template with all its properties:
   - Simple variables: service_name, container_name
-  - Dotted variables: traefik.host, network.name
-  - Dict variables: service_port['http'], nginx_dashboard.port['dashboard']
+  - Dotted variables: traefik.host, network.name, service_port.http
   - Enabler variables: Variables used in {% if var %} conditions
   """
   name: str
@@ -18,8 +17,6 @@ class TemplateVariable:
   
   # Variable characteristics
   is_enabler: bool = False  # Used in {% if %} conditions
-  is_dict: bool = False  # Has dict access patterns like var['key']
-  dict_keys: List[str] = field(default_factory=list)  # Keys accessed if is_dict
   
   # Grouping info (extracted from dotted notation)
   group: Optional[str] = None  # e.g., 'traefik' for 'traefik.host'
@@ -41,7 +38,6 @@ class TemplateVariable:
 def analyze_template_variables(
   vars_used: Set[str],
   var_defaults: Dict[str, Any],
-  var_dict_keys: Dict[str, List[str]],
   template_content: str
 ) -> Dict[str, TemplateVariable]:
   """Analyze template variables and create TemplateVariable objects.
@@ -49,7 +45,6 @@ def analyze_template_variables(
   Args:
     vars_used: Set of all variable names used in template
     var_defaults: Dict of variable defaults from template
-    var_dict_keys: Dict of variables with dict access patterns
     template_content: The raw template content for additional analysis
   
   Returns:
@@ -69,22 +64,16 @@ def analyze_template_variables(
     # Detect if it's an enabler
     var.is_enabler = var_name in enablers
     
-    # Detect if it's a dict variable
-    if var_name in var_dict_keys:
-      var.is_dict = True
-      var.dict_keys = var_dict_keys[var_name]
-      # Dict variables might have dict defaults
-      if isinstance(var.default, dict):
-        var.type = "dict"
-    
     # Infer type from default value
-    if var.default is not None and var.type == "string":
+    if var.default is not None:
       if isinstance(var.default, bool):
         var.type = "boolean"
       elif isinstance(var.default, int):
         var.type = "integer"
       elif isinstance(var.default, float):
         var.type = "float"
+      else:
+        var.type = "string"
     
     # If it's an enabler without a default, assume boolean
     if var.is_enabler and var.default is None:

+ 4 - 4
library/compose/nginx/compose.yaml

@@ -29,12 +29,12 @@ services:
         - traefik.http.routers.{{ container_name }}.service={{ container_name }}
       {% endif %}
     {% endif %}
-    {% if not traefik and service_port %}
+    {% if not traefik %}
     ports:
-      - "{{ service_port['http'] | default(8080) }}:80"
-      - "{{ service_port['https'] | default(8443) }}:443"
+      - "{{ service_port_http | default(8080) }}:80"
+      - "{{ service_port_https | default(8443) }}:443"
       {% if nginx_dashboard %}
-      - "{{ nginx_dashboard.port['dashboard'] | default(8081) }}:8080"
+      - "{{ nginx_dashboard_port_dashboard | default(8081) }}:8080"
       {% endif %}
     {% endif %}
     # volumes: