From ee62b4fce41574d4932dc9e52ac4520c0e321b8c Mon Sep 17 00:00:00 2001 From: Nathan O'Sullivan Date: Sat, 19 Jul 2025 09:15:25 +1000 Subject: [PATCH 1/3] refactor: extract mapping attribute formatting macros --- templates/endpoint_module/attributes.jinja | 47 +++++++++++++++++ .../endpoint_module/create_mapping.jinja | 51 +++---------------- 2 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 templates/endpoint_module/attributes.jinja diff --git a/templates/endpoint_module/attributes.jinja b/templates/endpoint_module/attributes.jinja new file mode 100644 index 00000000..8ba5153f --- /dev/null +++ b/templates/endpoint_module/attributes.jinja @@ -0,0 +1,47 @@ +{% macro add_primitive(property, argument, lookup_entity) %} + add(PrimitiveAttribute( + "{{ property.python_name }}", + {{ property.get_type_string() }}, + required = {{ property.required if not argument else True }}, + {{ option_name(property if not argument else None) }} + {% if not lookup_entity %} + {{ description_argument(property) }} + {% else %} + metavar="{{ property.python_name.replace("_" + lookup_entity['id'], "") }}", + {{ description_argument(property) + .replace(" " + lookup_entity['id'].upper(), " " + lookup_entity['id'].upper() + " or " + lookup_entity['ref']) + .replace(" " + lookup_entity['id'], " " + lookup_entity['id'] + " or " + lookup_entity['ref']) }} + lookup = lookup_{{ property.python_name }}, + {% endif %} + )) +{%- endmacro %} + +{% macro add_list(property) %} + add(ListAttribute( + "{{ property.python_name }}", + {{ property.inner_property.get_type_string(no_optional=True) }}, + required={{ property.required }}, + {{ option_name(property) }} + {{ description_argument(property) }} + )) +{% endmacro %} + +{% macro add_object(property, description_property) %} + add(ObjectAttribute( + "{{ property.python_name }}", + {{ property.get_type_string(no_optional=True) }}, + {{ option_name(property) }} + required = {{ property.required }}, + {{ description_argument(description_property) }} + )) +{% endmacro %} + +{% macro description_argument(prop) %} + {% if prop.description %} + description="""{{ prop.description.replace(" ","") }}""", + {% endif %} +{% endmacro %} + +{% macro option_name(prop) %} + option_name = {{ '"' + prop.name.replace('_','-') + '"' if prop else None }}, +{% endmacro %} diff --git a/templates/endpoint_module/create_mapping.jinja b/templates/endpoint_module/create_mapping.jinja index 10d22c71..f31b8532 100644 --- a/templates/endpoint_module/create_mapping.jinja +++ b/templates/endpoint_module/create_mapping.jinja @@ -1,8 +1,7 @@ +{% import "endpoint_module/attributes.jinja" as attributes %} def create_mapping(self) -> Mapping: mapping = Mapping(CommandRequest) {% if (req_parameters + opt_parameters) %} - {% macro description_argument(prop) %}{% if prop.description %}description="""{{ prop.description.replace(" ","") }}""",{% endif %}{% endmacro %} - {% macro option_name(prop) %}option_name = "{{ prop.name.replace('_','-') }}",{% endmacro %} {% for property in endpoint.path_parameters.values() %} {# Check if parameter has a lookup command, and define handler if it does #} @@ -13,21 +12,7 @@ return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref) {% endif %} - mapping.add(PrimitiveAttribute( - "{{ property.python_name }}", - {{ property.get_type_string() }}, - required = True, - option_name = None, - {% if not lookup_endpoint %} - {{ description_argument(property) }} - {% else %} - metavar="{{ property.python_name.replace("_" + lookup_entity['id'], "") }}", - {{ description_argument(property) - .replace(" " + lookup_entity['id'].upper(), " " + lookup_entity['id'].upper() + " or " + lookup_entity['ref']) - .replace(" " + lookup_entity['id'], " " + lookup_entity['id'] + " or " + lookup_entity['ref']) }} - lookup = lookup_{{ property.python_name }}, - {% endif %} - )) + mapping.{{ attributes.add_primitive(property, True, lookup_entity if lookup_endpoint else None) }} {% endfor %} {% if endpoint.json_body %} @@ -40,34 +25,16 @@ {% if property.get_instance_type_string() == "list" and property.inner_property.class_info in openapi.models_by_class %} {% set subparser = parser + "_" + property.inner_property.class_info.module_name %} - {{ subparser }} = {{ parser }}.add(ListAttribute( - "{{ property.python_name }}", - {{ property.inner_property.get_type_string(no_optional=True) }}, - required={{ property.required }}, - {{ option_name(property) }} - {{ description_argument(property) }} - )) + {{ subparser }} = {{ parser }}.{{ attributes.add_list(property) }} {{ add_object_to_parser(subparser, openapi.models_by_class[property.inner_property.class_info]) }} {% elif property.class_info in openapi.models_by_class %} {% set subparser = parser + "_" + property.class_info.module_name %} - {{ subparser }} = {{ parser }}.add(ObjectAttribute( - "{{ property.python_name }}", - {{ property.get_type_string(no_optional=True) }}, - {{ option_name(property) }} - required = {{ property.required }}, - {{ description_argument(prop.data.properties[property.name]) }} - )) + {{ subparser }} = {{ parser }}.{{ attributes.add_object(property, prop.data.properties[property.name]) }} {{ add_object_to_parser(subparser, openapi.models_by_class[property.class_info]) }} {% else %} - {{ parser }}.add(PrimitiveAttribute( - "{{ property.python_name }}", - {{ property.get_type_string() }}, - required={{ property.required }}, - {{ option_name(property) }} - {{ description_argument(property) }} - )) + {{ parser }}.{{ attributes.add_primitive(property) }} {% endif %} {% endfor %} {% endmacro %} @@ -82,13 +49,7 @@ {% if endpoint.query_parameters %} {% for property in endpoint.query_parameters.values() if property.name not in ('page', 'per_page') %} - mapping.add(PrimitiveAttribute( - "{{ property.python_name }}", - {{ property.get_type_string() }}, - required={{ property.required }}, - {{ option_name(property) }} - {{ description_argument(property) }} - )) + mapping.{{ attributes.add_primitive(property) }} {% endfor %} {% endif %} {% endif %} From acbe9b1ac5f3269f110ade6d634c21ddb8574533 Mon Sep 17 00:00:00 2001 From: Nathan O'Sullivan Date: Tue, 22 Jul 2025 12:13:30 +1000 Subject: [PATCH 2/3] feat: support x-cli-lookup attribute on request body properties --- ...load_balancers_load_balancer_id_servers.py | 10 +++- .../commands/api/post_v2_load_balancers.py | 10 +++- ...load_balancers_load_balancer_id_servers.py | 10 +++- .../put_v2_load_balancers_load_balancer_id.py | 10 +++- src/binarylane/console/parser/attribute.py | 18 ++++-- .../console/parser/object_attribute.py | 9 +-- src/binarylane/console/parser/parser.py | 9 ++- .../console/parser/primitive_attribute.py | 6 +- templates/endpoint_module/attributes.jinja | 58 +++++++++++++++++-- .../endpoint_module/create_mapping.jinja | 16 ++--- templates/endpoint_module/lookups.jinja | 26 ++++++--- templates/lookups.jinja | 41 ++++++++++--- 12 files changed, 167 insertions(+), 56 deletions(-) diff --git a/src/binarylane/console/commands/api/delete_v2_load_balancers_load_balancer_id_servers.py b/src/binarylane/console/commands/api/delete_v2_load_balancers_load_balancer_id_servers.py index 8332c02f..526c5abf 100644 --- a/src/binarylane/console/commands/api/delete_v2_load_balancers_load_balancer_id_servers.py +++ b/src/binarylane/console/commands/api/delete_v2_load_balancers_load_balancer_id_servers.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from binarylane.client import Client +import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers from binarylane.console.parser import Mapping, PrimitiveAttribute from binarylane.console.runners.command import CommandRunner @@ -44,13 +45,18 @@ def create_mapping(self) -> Mapping: json_body = mapping.add_json_body(ServerIdsRequest) + def lookup_server_id(ref: str) -> Union[None, int]: + return api_get_v2_servers.Command(self._context).lookup(ref) + json_body.add( PrimitiveAttribute( "server_ids", List[int], required=True, - option_name="server-ids", - description="""A list of server IDs.""", + option_name=("servers", "server-ids"), + metavar="servers", + description="""A list of server ID or names.""", + lookup=lookup_server_id, ) ) diff --git a/src/binarylane/console/commands/api/post_v2_load_balancers.py b/src/binarylane/console/commands/api/post_v2_load_balancers.py index 75989a9d..63a535b9 100644 --- a/src/binarylane/console/commands/api/post_v2_load_balancers.py +++ b/src/binarylane/console/commands/api/post_v2_load_balancers.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from binarylane.client import Client +import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers from binarylane.console.parser import ListAttribute, Mapping, ObjectAttribute, PrimitiveAttribute from binarylane.console.runners.actionlink import ActionLinkRunner @@ -112,13 +113,18 @@ def create_mapping(self) -> Mapping: ) ) + def lookup_server_id(ref: str) -> Union[None, int]: + return api_get_v2_servers.Command(self._context).lookup(ref) + json_body.add( PrimitiveAttribute( "server_ids", Union[Unset, None, List[int]], required=False, - option_name="server-ids", - description="""A list of server IDs to assign to this load balancer.""", + option_name=("servers", "server-ids"), + metavar="servers", + description="""A list of server ID or names to assign to this load balancer.""", + lookup=lookup_server_id, ) ) diff --git a/src/binarylane/console/commands/api/post_v2_load_balancers_load_balancer_id_servers.py b/src/binarylane/console/commands/api/post_v2_load_balancers_load_balancer_id_servers.py index c355f8d5..5085a58b 100644 --- a/src/binarylane/console/commands/api/post_v2_load_balancers_load_balancer_id_servers.py +++ b/src/binarylane/console/commands/api/post_v2_load_balancers_load_balancer_id_servers.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from binarylane.client import Client +import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers from binarylane.console.parser import Mapping, PrimitiveAttribute from binarylane.console.runners.command import CommandRunner @@ -44,13 +45,18 @@ def create_mapping(self) -> Mapping: json_body = mapping.add_json_body(ServerIdsRequest) + def lookup_server_id(ref: str) -> Union[None, int]: + return api_get_v2_servers.Command(self._context).lookup(ref) + json_body.add( PrimitiveAttribute( "server_ids", List[int], required=True, - option_name="server-ids", - description="""A list of server IDs.""", + option_name=("servers", "server-ids"), + metavar="servers", + description="""A list of server ID or names.""", + lookup=lookup_server_id, ) ) diff --git a/src/binarylane/console/commands/api/put_v2_load_balancers_load_balancer_id.py b/src/binarylane/console/commands/api/put_v2_load_balancers_load_balancer_id.py index 87a72dda..c5ec53db 100644 --- a/src/binarylane/console/commands/api/put_v2_load_balancers_load_balancer_id.py +++ b/src/binarylane/console/commands/api/put_v2_load_balancers_load_balancer_id.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from binarylane.client import Client +import binarylane.console.commands.api.get_v2_servers as api_get_v2_servers from binarylane.console.parser import ListAttribute, Mapping, ObjectAttribute, PrimitiveAttribute from binarylane.console.runners.command import CommandRunner @@ -125,13 +126,18 @@ def create_mapping(self) -> Mapping: ) ) + def lookup_server_id(ref: str) -> Union[None, int]: + return api_get_v2_servers.Command(self._context).lookup(ref) + json_body.add( PrimitiveAttribute( "server_ids", Union[Unset, None, List[int]], required=False, - option_name="server-ids", - description="""A list of server IDs to assign to this load balancer.""", + option_name=("servers", "server-ids"), + metavar="servers", + description="""A list of server ID or names to assign to this load balancer.""", + lookup=lookup_server_id, ) ) diff --git a/src/binarylane/console/parser/attribute.py b/src/binarylane/console/parser/attribute.py index 4b6a30c4..af7ce98a 100644 --- a/src/binarylane/console/parser/attribute.py +++ b/src/binarylane/console/parser/attribute.py @@ -2,7 +2,7 @@ import logging from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, ClassVar, List, Optional +from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence, Union logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class Attribute(ABC): init: bool # if required is True, this attribute is mandatory in the command-line parsing sense required: bool - option_name: Optional[str] + option_names: List[str] description: Optional[str] def __init__( @@ -32,14 +32,14 @@ def __init__( attribute_type: type, *, required: bool, - option_name: Optional[str], + option_name: Union[str, Sequence[str], None], description: Optional[str], ) -> None: self.attribute_name = attribute_name self.attribute_type = attribute_type self.init = required self.required = required - self.option_name = option_name + self.option_names = [option_name] if isinstance(option_name, str) else list(option_name) if option_name else [] self.description = description @property @@ -51,8 +51,14 @@ def usage(self) -> Optional[str]: return None @property - def name_or_flag(self) -> str: - return f"--{self.option_name}" if self.option_name else self.attribute_name + def option_name(self) -> Optional[str]: + return self.option_names[0] if self.option_names else None + + @property + def name_or_flag(self) -> Sequence[str]: + if not self.option_names: + return [self.attribute_name] + return [f"--{opt}" for opt in self.option_names] @property def attributes(self) -> List[Attribute]: diff --git a/src/binarylane/console/parser/object_attribute.py b/src/binarylane/console/parser/object_attribute.py index 99f7f38c..80e8698f 100644 --- a/src/binarylane/console/parser/object_attribute.py +++ b/src/binarylane/console/parser/object_attribute.py @@ -72,11 +72,10 @@ def configure(self, parser: Parser) -> None: existing_arguments = parser.argument_names # If any argument names for this class conflict with existing names, prefix all the argument names - if any(arg for arg in self.attributes if arg.name_or_flag in existing_arguments): + if any(arg for arg in self.attributes if any(opt for opt in arg.name_or_flag if opt in existing_arguments)): self._unsupported("Prefixing option names", False) for arg in self.attributes: - if arg.option_name: - arg.option_name = f"{self.attribute_name.replace('_', '-')}-{arg.option_name}" + arg.option_names = [f"{self.attribute_name.replace('_', '-')}-{opt}" for opt in arg.option_names] group = self.group_name if group: @@ -97,7 +96,9 @@ def construct(self, parser: Parser, parsed: argparse.Namespace) -> object: # If there are required attributes for the class constructor if init_kwargs: # See if any were not provided a value - missing = [attr.name_or_flag for attr in self.init_attributes if init_kwargs[attr.attribute_name] is UNSET] + missing = [ + attr.name_or_flag[0] for attr in self.init_attributes if init_kwargs[attr.attribute_name] is UNSET + ] # If one or more required attributes did not receive a value: if missing: diff --git a/src/binarylane/console/parser/parser.py b/src/binarylane/console/parser/parser.py index 6076daa0..57dde76a 100644 --- a/src/binarylane/console/parser/parser.py +++ b/src/binarylane/console/parser/parser.py @@ -133,7 +133,8 @@ def remove_group(self, group_name: str) -> None: del self._groups[group_name] self._action_groups = [g for g in self._action_groups if g.title != group_name] - def add_to_group(self, group_name: Union[str, bool], name_or_flag: str, type_: type, **kwargs: Any) -> None: + def add_to_group(self, group_name: Union[str, bool], names: Sequence[str], type_: type, **kwargs: Any) -> None: + name_or_flag, *aliases = names self._argument_names.append(name_or_flag) if isinstance(group_name, bool): @@ -150,4 +151,8 @@ def add_to_group(self, group_name: Union[str, bool], name_or_flag: str, type_: t del kwargs["required"] logger.debug("add_argument %s (%s) - %s", name_or_flag, type_, repr(kwargs)) - group.add_argument(name_or_flag, type=type_, **kwargs) + + action = group.add_argument(*names, type=type_, **kwargs) + # Do not show option aliases in help output + if aliases: + action.option_strings = [name_or_flag] diff --git a/src/binarylane/console/parser/primitive_attribute.py b/src/binarylane/console/parser/primitive_attribute.py index 9123a7b9..ef2804ab 100644 --- a/src/binarylane/console/parser/primitive_attribute.py +++ b/src/binarylane/console/parser/primitive_attribute.py @@ -4,7 +4,7 @@ import logging from datetime import datetime from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Type, Union from binarylane.pycompat import actions, typing from binarylane.types import UNSET, Unset @@ -47,7 +47,7 @@ def __init__( attribute_name: str, attribute_type_hint: object, *, - option_name: Optional[str], + option_name: Union[Sequence[str], str, None], required: bool, description: Optional[str] = None, metavar: Optional[str] = None, @@ -72,7 +72,7 @@ def __init__( self._dest = attribute_name self._action = action self._lookup = lookup - self._metavar = (metavar or option_name or attribute_name).replace("-", "_").upper() + self._metavar = (metavar or self.option_name or attribute_name).replace("-", "_").upper() @property def usage(self) -> Optional[str]: diff --git a/templates/endpoint_module/attributes.jinja b/templates/endpoint_module/attributes.jinja index 8ba5153f..b242d4b0 100644 --- a/templates/endpoint_module/attributes.jinja +++ b/templates/endpoint_module/attributes.jinja @@ -1,17 +1,43 @@ -{% macro add_primitive(property, argument, lookup_entity) %} +{% macro add_lookup(property) %} + {% set lookup_endpoint = lookup_parameters.get(property.name) %} + {% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %} + {% set lookup_type = property.get_instance_type_string() %} + {% if lookup_type == 'list' %} + {% set lookup_type = property.inner_property.get_instance_type_string() %} + {% endif %} + {% if lookup_endpoint -%} + def {{ lookup_function(property, lookup_entity.id) }}(ref: str) -> Union[None, {{ lookup_type }}]: + return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref) + + {% endif %} +{% endmacro %} + +{% macro add_primitive(property, argument) %} add(PrimitiveAttribute( "{{ property.python_name }}", {{ property.get_type_string() }}, required = {{ property.required if not argument else True }}, + {% set lookup_endpoint = lookup_parameters.get(property.name) %} + {% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %} + {% if not lookup_endpoint %} {{ option_name(property if not argument else None) }} - {% if not lookup_entity %} {{ description_argument(property) }} {% else %} - metavar="{{ property.python_name.replace("_" + lookup_entity['id'], "") }}", + {% set option_name = property.name.replace('_','-') %} + {% set entity_id = lookup_entity['id'] %} + {% if argument %} + option_name = None, + {% elif option_name.endswith("-" + entity_id) or option_name.endswith("-" + entity_id + "s") %} + option_name = ("{{ remove_suffix(option_name, "-" + entity_id) }}", "{{ option_name }}"), + {% else %} + option_name = "{{ option_name }}", + {% endif %} + {% set entity_ref = lookup_entity['ref'] %} + metavar="{{ remove_suffix(property.python_name, "_" + entity_id) }}", {{ description_argument(property) - .replace(" " + lookup_entity['id'].upper(), " " + lookup_entity['id'].upper() + " or " + lookup_entity['ref']) - .replace(" " + lookup_entity['id'], " " + lookup_entity['id'] + " or " + lookup_entity['ref']) }} - lookup = lookup_{{ property.python_name }}, + .replace(" " + entity_id.upper(), " " + entity_id.upper() + " or " + entity_ref) + .replace(" " + entity_id, " " + entity_id + " or " + entity_ref) }} + lookup = {{ lookup_function(property, entity_id )}} {% endif %} )) {%- endmacro %} @@ -45,3 +71,23 @@ {% macro option_name(prop) %} option_name = {{ '"' + prop.name.replace('_','-') + '"' if prop else None }}, {% endmacro %} + +{% macro lookup_function(property, entity_id) %} +{% filter trim %} +{% set suffix = "_" + entity_id %} +{# Normalize property.python_name of "server"/"server_id"/"server_ids" to "server_id" #} +lookup_{{ property.python_name.removesuffix(suffix+"s").removesuffix(suffix) }}{{ suffix }} +{% endfilter %} +{% endmacro %} + +{% macro remove_suffix(value, suffix) %} +{% filter trim %} +{% if value.endswith(suffix + "s") %} +{{ value.removesuffix(suffix + "s") }}s +{% elif value.endswith(suffix) %} +{{ value.removesuffix(suffix) }} +{% else %} +{{ value }} +{% endif %} +{% endfilter %} +{% endmacro %} diff --git a/templates/endpoint_module/create_mapping.jinja b/templates/endpoint_module/create_mapping.jinja index f31b8532..f7b58702 100644 --- a/templates/endpoint_module/create_mapping.jinja +++ b/templates/endpoint_module/create_mapping.jinja @@ -1,18 +1,13 @@ -{% import "endpoint_module/attributes.jinja" as attributes %} +{% import "endpoint_module/attributes.jinja" as attributes with context %} def create_mapping(self) -> Mapping: mapping = Mapping(CommandRequest) {% if (req_parameters + opt_parameters) %} {% for property in endpoint.path_parameters.values() %} - {# Check if parameter has a lookup command, and define handler if it does #} - {% set lookup_endpoint = lookup_parameters.get(property.name) %} - {% if lookup_endpoint %} - {% set lookup_entity = lookup_endpoint.data['x-cli-entity'] %} - def lookup_{{ property.python_name }}(ref: str) -> Union[None, {{ property.get_type_string() }}]: - return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref) - - {% endif %} - mapping.{{ attributes.add_primitive(property, True, lookup_entity if lookup_endpoint else None) }} + {{ attributes.add_lookup(property) }} + {% endfor %} + {% for property in endpoint.path_parameters.values() %} + mapping.{{ attributes.add_primitive(property, True) }} {% endfor %} {% if endpoint.json_body %} @@ -34,6 +29,7 @@ {{ add_object_to_parser(subparser, openapi.models_by_class[property.class_info]) }} {% else %} + {{ attributes.add_lookup(property) }} {{ parser }}.{{ attributes.add_primitive(property) }} {% endif %} {% endfor %} diff --git a/templates/endpoint_module/lookups.jinja b/templates/endpoint_module/lookups.jinja index f8704fe5..8c2ad34d 100644 --- a/templates/endpoint_module/lookups.jinja +++ b/templates/endpoint_module/lookups.jinja @@ -1,11 +1,19 @@ +{% filter trim %}{# Discard whitespace #} +{# Populate dictionary of parameter -> lookup endpoint for attributes with x-cli-lookup #} -{# Create a dictionary of path parameter -> endpoint (defined in endpoint_module.py.jinja due to jinja scope behaviour) #} -{% for property in endpoint.path_parameters.values() %} -{% for param in endpoint.data.parameters if param.param_in == 'path' and property.name == param.name and param['x-cli-lookup'] %} -{% set lookup_endpoint = openapi.endpoints_by_name.get(param['x-cli-lookup']) %} -{% if not lookup_endpoint %} -{{ fail('lookup command not found', param['x-cli-lookup']) }} -{% endif %} -{% set _ = lookup_parameters.__setitem__(property.name, lookup_endpoint) %} -{% endfor %} +{# Examine path parameters #} +{% for param in endpoint.data.parameters or [] if param['x-cli-lookup'] %} + {% set _ = lookup_parameters.__setitem__(param.name, openapi.endpoints_by_name.get(param['x-cli-lookup'])) %} {% endfor %} + +{# Examine request body schema, if any #} +{% if endpoint.json_body and endpoint.json_body.get_instance_type_string() != 'list' %} + {% set model = openapi.models_by_class[endpoint.json_body.class_info] %} + {% if model and model.data %} + {% for name, prop in model.data.properties.items() if prop['x-cli-lookup'] %} + {% set _ = lookup_parameters.__setitem__(name, openapi.endpoints_by_name.get(prop['x-cli-lookup'])) %} + {% endfor %} + {% endif %} +{% endif %} + +{% endfilter %} diff --git a/templates/lookups.jinja b/templates/lookups.jinja index 8db511ab..cdac0be3 100644 --- a/templates/lookups.jinja +++ b/templates/lookups.jinja @@ -1,17 +1,42 @@ -{# TODO: implement lookups via OpenAPI spec metadata #} +{% filter trim %}{# Discard whitespace #} + +{# --- +This template synthesises "x-cli-lookup" and "x-cli-entity" extension properties +on path parameter and request schema definitions as the required data is not +available in OpenAPI Specification published by BinaryLane. +--- #} + +{# Map of attribute name to command that can resolve attribute reference. #} {% set lookup_map = { "server_id": "server list", } %} + +{# Map of command to {id,ref} obect specifying the attributes within +x-cli-command's response that provide the reference (input) and id (output). #} {% set entity_map = { "server list": {"id": "id", "ref": "name"}, } %} + +{# Add x-cli-lookup to attributes that support lookup. #} {% for parameter_name in lookup_map %} -{% for param in (endpoint.data.parameters or []) if param.param_in == "path" and param.name == parameter_name %} -{% set _ = param.__setattr__("x-cli-lookup", lookup_map[parameter_name]) %} + + {# Examine path parameters #} + {% for param in (endpoint.data.parameters or []) if param.param_in == "path" and param.name == parameter_name %} + {% set _ = param.__setattr__("x-cli-lookup", lookup_map[parameter_name]) %} + {% endfor %} + + {# Examine request body schema #} + {% if endpoint.json_body and endpoint.json_body.get_instance_type_string() != 'list' %} + {% for name, prop in openapi.models_by_class[endpoint.json_body.class_info].data.properties.items() if name in ["%s","%ss"] | map("format", parameter_name) %} + {% set _ = prop.__setattr__("x-cli-lookup", lookup_map[parameter_name]) %} + {% endfor %} + {% endif %} + {% endfor %} + +{# Add x-cli-entity if endpoint is is used to perform lookups. #} +{% for command_name in entity_map if endpoint.data["x-cli-command"] == command_name %} + {% set _ = endpoint.data.__setattr__("x-cli-entity", entity_map[command_name]) %} {% endfor %} -{% for command_name in entity_map %} -{% if endpoint.data["x-cli-command"] == command_name %} -{% set _ = endpoint.data.__setattr__("x-cli-entity", entity_map[command_name]) %} -{% endif %} -{% endfor %} + +{% endfilter %} From 7084d2464c957f5715007a1feb1cbe9f85240d5a Mon Sep 17 00:00:00 2001 From: Nathan O'Sullivan Date: Tue, 22 Jul 2025 12:36:58 +1000 Subject: [PATCH 3/3] whitespace changes --- templates/endpoint_module/attributes.jinja | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/templates/endpoint_module/attributes.jinja b/templates/endpoint_module/attributes.jinja index b242d4b0..5d7d9d91 100644 --- a/templates/endpoint_module/attributes.jinja +++ b/templates/endpoint_module/attributes.jinja @@ -1,15 +1,15 @@ {% macro add_lookup(property) %} - {% set lookup_endpoint = lookup_parameters.get(property.name) %} - {% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %} - {% set lookup_type = property.get_instance_type_string() %} - {% if lookup_type == 'list' %} - {% set lookup_type = property.inner_property.get_instance_type_string() %} - {% endif %} - {% if lookup_endpoint -%} + {% set lookup_endpoint = lookup_parameters.get(property.name) %} + {% set lookup_entity = lookup_endpoint.data["x-cli-entity"] if lookup_endpoint %} + {% set lookup_type = property.get_instance_type_string() %} + {% if lookup_type == 'list' %} + {% set lookup_type = property.inner_property.get_instance_type_string() %} + {% endif %} + {% if lookup_endpoint -%} def {{ lookup_function(property, lookup_entity.id) }}(ref: str) -> Union[None, {{ lookup_type }}]: return api_{{ python_identifier(lookup_endpoint.name) }}.Command(self._context).lookup(ref) - {% endif %} + {% endif %} {% endmacro %} {% macro add_primitive(property, argument) %} @@ -74,20 +74,20 @@ {% macro lookup_function(property, entity_id) %} {% filter trim %} -{% set suffix = "_" + entity_id %} -{# Normalize property.python_name of "server"/"server_id"/"server_ids" to "server_id" #} -lookup_{{ property.python_name.removesuffix(suffix+"s").removesuffix(suffix) }}{{ suffix }} + {% set suffix = "_" + entity_id %} + {# Normalize property.python_name of "server"/"server_id"/"server_ids" to "server_id" #} + lookup_{{ property.python_name.removesuffix(suffix+"s").removesuffix(suffix) }}{{ suffix }} {% endfilter %} {% endmacro %} {% macro remove_suffix(value, suffix) %} {% filter trim %} -{% if value.endswith(suffix + "s") %} -{{ value.removesuffix(suffix + "s") }}s -{% elif value.endswith(suffix) %} -{{ value.removesuffix(suffix) }} -{% else %} -{{ value }} -{% endif %} + {% if value.endswith(suffix + "s") %} + {{ value.removesuffix(suffix + "s") }}s + {% elif value.endswith(suffix) %} + {{ value.removesuffix(suffix) }} + {% else %} + {{ value }} + {% endif %} {% endfilter %} {% endmacro %}