def parse_element_arg(element_arg: str, contextual_mode: bool = True) -> List[ElementPath]: """Parse an element arg string into a list of ElementPath. contextual_mode specifies if the path is a valid project model path or not. For example, if we are processing a metadata.parties.*, we need to know which metadata we are processing. If we pass contextual_mode=True, we can infer the root model by inspecting the file directory If contextual_mode=False, then the path must include the full path, e.g. catalog.metadata.parties.* instead of just metadata.parties.* One option for caller to utilize this utility function: fs.is_valid_project_model_path(pathlib.Path.cwd()) """ element_paths: List[ElementPath] = [] element_arg = element_arg.strip() # search for wildcards and create paths with its parent path path_parts = element_arg.split(ElementPath.PATH_SEPARATOR) if len(path_parts) <= 0: raise TrestleError(f'Invalid element path "{element_arg}" without any path separator') prev_element_path = None parent_model = path_parts[0] i = 1 while i < len(path_parts): p = path_parts[i] if p == ElementPath.WILDCARD and len(element_paths) > 0: # append wildcard to the latest element path latest_path = element_paths.pop() if latest_path.get_last() == ElementPath.WILDCARD: raise TrestleError(f'Invalid element path with consecutive {ElementPath.WILDCARD}') latest_path_str = ElementPath.PATH_SEPARATOR.join([latest_path.to_string(), p]) element_path = ElementPath(latest_path_str, latest_path.get_parent()) else: # create and append elment_path p = ElementPath.PATH_SEPARATOR.join([parent_model, p]) element_path = ElementPath(p, parent_path=prev_element_path) # if the path has wildcard and there is more parts later, # get the parent model for the alias path if element_path.get_last() == ElementPath.WILDCARD: full_path_str = ElementPath.PATH_SEPARATOR.join(element_path.get_full_path_parts()[:-1]) parent_model = fs.get_singular_alias(full_path_str, contextual_mode) else: parent_model = element_path.get_element_name() # store values for next cycle prev_element_path = element_path element_paths.append(element_path) i += 1 if len(element_paths) <= 0: raise TrestleError(f'Invalid element path "{element_arg}" without any path separator') return element_paths
def parse_chain( model_obj: Union[OscalBaseModel, None], path_parts: List[str], relative_path: Optional[pathlib.Path] = None ) -> List[ElementPath]: """Parse the model chain starting from the beginning. Args: model_obj: Model to use for inspecting available elements, if available or none path_parts: list of string paths to parse including wildcards relative_path: Optional relative path (w.r.t trestle project root directory) Returns: List of ElementPath """ element_paths: List[ElementPath] = [] sub_model = model_obj have_model_to_parse = model_obj is not None prev_element_path = None latest_path = None parent_model = path_parts[0] i = 1 while i < len(path_parts): p = path_parts[i] # if hit wildcard create element path up to this point if p == ElementPath.WILDCARD and len(element_paths) > 0: # append wildcard to the latest element path latest_path = element_paths.pop() if latest_path.get_last() == ElementPath.WILDCARD: raise TrestleError(f'Invalid element path with consecutive {ElementPath.WILDCARD}') latest_path_str = ElementPath.PATH_SEPARATOR.join([latest_path.to_string(), p]) element_path = ElementPath(latest_path_str, latest_path.get_parent()) else: # create and append element_path # at this point sub_model may be a list of items # new element path is needed only if any of the items contains the desired part if p != ElementPath.WILDCARD: new_attrib = str_utils.dash_to_underscore(p) if isinstance(sub_model, list): for item in sub_model: # go into the list and find one with requested part sub_item = getattr(item, new_attrib, None) if sub_item is not None: sub_model = sub_item break else: sub_model = getattr(sub_model, new_attrib, None) if have_model_to_parse and sub_model is None: return element_paths p = ElementPath.PATH_SEPARATOR.join([parent_model, p]) element_path = ElementPath(p, parent_path=prev_element_path) # If the path has wildcard and there are more parts later, # get the parent model for the alias path # If path has wildcard and it does not refer to a list, then there can be nothing after * if element_path.get_last() == ElementPath.WILDCARD: full_path_str = ElementPath.PATH_SEPARATOR.join(element_path.get_full_path_parts()[:-1]) parent_model = ModelUtils.get_singular_alias(full_path_str, relative_path) # Does wildcard mean we need to inspect the sub_model to determine what can be split off from it? # If it has __root__ it may mean it contains a list of objects and should be split as a list if isinstance(sub_model, OscalBaseModel): root = getattr(sub_model, '__root__', None) if root is None or not isinstance(root, list): # Cannot have parts beyond * if it isn't a list if i < len(path_parts) - 1: raise TrestleError( f'Cannot split beyond * when the wildcard does not refer to a list. Path: {path_parts}' ) for key in sub_model.__fields__.keys(): # only create element path is item is present in the sub_model if getattr(sub_model, key, None) is None: continue new_alias = str_utils.underscore_to_dash(key) new_path = full_path_str + '.' + new_alias if not split_is_too_fine(new_path, model_obj): # to add parts of an element, need to add two links # prev_element_path may be None, for example catalog.* if prev_element_path is not None: element_paths.append(prev_element_path) element_paths.append(ElementPath(parent_model + '.' + new_alias, latest_path)) # Since wildcard is last in the chain when splitting an oscal model we are done return element_paths else: parent_model = element_path.get_element_name() # store values for next cycle prev_element_path = element_path element_paths.append(element_path) i += 1 return element_paths