""" This module parses a json/yaml file that defines a flow of questions to fulfill the cookiecutter context""" from typing import Dict, Optional, Tuple import yaml from samcli.commands.exceptions import UserException from samcli.yamlhelper import parse_yaml_file from .interactive_flow import InteractiveFlow from .question import Question, QuestionFactory class QuestionsNotFoundException(UserException): pass class QuestionsFailedParsingException(UserException): pass class InteractiveFlowCreator: @staticmethod def create_flow(flow_definition_path: str, extra_context: Optional[Dict] = None) -> InteractiveFlow: """ This method parses the given json/yaml file to create an InteractiveFLow. It expects the file to define a list of questions. It parses the questions and add it to the flow in the same order they are defined in the file, i.e. the default next-question of a given question will be the next one defined in the file, while also respecting the question-defined _next_question map if provided. Parameters: ---------- flow_definition_path: str A path to a json/yaml file that defines the questions of the flow. the file is expected to be in the following format: { "questions": [ { "key": "key of the corresponding cookiecutter config", "question": "the question to prompt to the user", "kind": "the kind of the question, for example: confirm", "isRequired": true/false, # optional branching logic to jump to a particular question based on the answer. if not given # will automatically go to next question "nextQuestion": { "True": "key of the question to jump to if the user answered 'Yes'", "False": "key of the question to jump to if the user answered 'Yes'", } "default": "default_answer", # the default value can also be loaded from cookiecutter context # with a key path whose key path item can be loaded from cookiecutter as well. "default": { "keyPath": [ { "valueOf": "key-of-another-question" }, "pipeline_user" ] } # assuming the answer of "key-of-another-question" is "ABC" # the default value will be load from cookiecutter context with key "['ABC', 'pipeline_user]" }, ... ] } extra_context: Dict if the template contains variable($variableName) this parameter provides the values for those variables. Returns: InteractiveFlow(questions={k1: q1, k2: q2, ...}, first_question_key="first question's key") """ questions, first_question_key = InteractiveFlowCreator._load_questions(flow_definition_path, extra_context) return InteractiveFlow(questions=questions, first_question_key=first_question_key) @staticmethod def _load_questions( flow_definition_path: str, extra_context: Optional[Dict] = None ) -> Tuple[Dict[str, Question], str]: previous_question: Optional[Question] = None first_question_key: str = "" questions: Dict[str, Question] = {} questions_definition = InteractiveFlowCreator._parse_questions_definition(flow_definition_path, extra_context) try: for question in questions_definition.get("questions", []): q = QuestionFactory.create_question_from_json(question) if not first_question_key: first_question_key = q.key elif previous_question and not previous_question.default_next_question_key: previous_question.set_default_next_question_key(q.key) questions[q.key] = q previous_question = q return questions, first_question_key except (KeyError, ValueError, AttributeError, TypeError) as ex: raise QuestionsFailedParsingException(f"Failed to parse questions: {str(ex)}") from ex @staticmethod def _parse_questions_definition(file_path: str, extra_context: Optional[Dict] = None) -> Dict: """ Read the questions definition file, do variable substitution, parse it as JSON/YAML Parameters ---------- file_path : string Path to the questions definition to read extra_context : Dict if the file contains variable($variableName) this parameter provides the values for those variables. Raises ------ QuestionsNotFoundException: if the file_path doesn't exist QuestionsFailedParsingException: if any error occurred during variables substitution or content parsing Returns ------- questions data as a dictionary """ try: return parse_yaml_file(file_path=file_path, extra_context=extra_context) except FileNotFoundError as ex: raise QuestionsNotFoundException(f"questions definition file not found at {file_path}") from ex except (KeyError, ValueError, yaml.YAMLError) as ex: raise QuestionsFailedParsingException(f"Failed to parse questions: {str(ex)}") from ex