// mateQuestionnaire provides functions to create and read mate questionnaires and questionnaire responses. // Questionnaires are lists of questions that users can share on their profile. // Users can filter matches based on their responses. // Full questionnaire specification is provided in /documentation/Specification.md package mateQuestionnaire import "seekia/internal/allowedText" import "seekia/internal/encoding" import "seekia/internal/helpers" import "strings" import "errors" type QuestionObject struct{ // 9 or 10 byte hex encoded string // 9 bytes = Choice // 10 bytes = Entry Identifier string // "Choice"/"Entry" Type string // The content of the question // Example: "What is your job?" Content string Options string } //Will also verify questionnaire func ReadQuestionnaireString(inputQuestionnaire string)([]QuestionObject, error){ if (inputQuestionnaire == ""){ return nil, errors.New("ReadQuestionnaireString called with empty questionnaire.") } questionsRawList := strings.Split(inputQuestionnaire, "+&") if (len(questionsRawList) > 25){ return nil, errors.New("ReadQuestionnaireString called with questionnaire containing more than 25 questions.") } // We use this map to detect duplicates questionIdentifiersMap := make(map[string]struct{}) questionnaireQuestionsList := make([]QuestionObject, 0, len(questionsRawList)) for _, rawQuestion := range questionsRawList{ questionInfoList := strings.Split(rawQuestion, "%¢") if (len(questionInfoList) != 4){ return nil, errors.New("Malformed questionnaire question: " + rawQuestion) } questionIdentifier := questionInfoList[0] questionType := questionInfoList[1] questionContent := questionInfoList[2] questionOptions := questionInfoList[3] getExpectedQuestionIdentifierLength := func()uint32{ if (questionType == "Choice"){ return 9 } return 10 } expectedQuestionIdentifierLength := getExpectedQuestionIdentifierLength() isValid := helpers.VerifyHexString(expectedQuestionIdentifierLength, questionIdentifier) if (isValid == false){ return nil, errors.New("Malformed question: Invalid identifier: " + questionIdentifier) } _, exists := questionIdentifiersMap[questionIdentifier] if (exists == true){ return nil, errors.New("Malformed questionnaire: Duplicate questionIdentifier exists.") } questionIdentifiersMap[questionIdentifier] = struct{}{} if (questionType != "Choice" && questionType != "Entry"){ return nil, errors.New("Malformed question: Invalid question type: " + questionType) } if (questionContent == ""){ return nil, errors.New("Malformed question: Empty content.") } isAllowed := allowedText.VerifyStringIsAllowed(questionContent) if (isAllowed == false){ return nil, errors.New("Malformed question: Content contains unallowed text") } if (len(questionContent) > 500){ return nil, errors.New("Malformed question: Content is too long.") } if (questionType == "Choice"){ maxChoicesAllowed, choicesListString, delimiterFound := strings.Cut(questionOptions, "#") if (delimiterFound == false){ return nil, errors.New("Malformed question: Invalid choice question options: Missing #") } maxChoicesAllowedInt, err := helpers.ConvertStringToInt(maxChoicesAllowed) if (err != nil) { return nil, errors.New("Malformed question: Invalid maximum choices allowed.") } if (maxChoicesAllowedInt < 1 || maxChoicesAllowedInt > 6){ return nil, errors.New("Malformed question: Invalid max choices allowed") } choicesList := strings.Split(choicesListString, "$¥") if (len(choicesList) < 2 || len(choicesList) > 6){ return nil, errors.New("Malformed question: Invalid choices list length.") } // We use this map to check for duplicates choicesMap := make(map[string]struct{}) for _, choiceString := range choicesList{ if (choiceString == ""){ return nil, errors.New("Malformed question: Choice is empty.") } if (len(choiceString) > 100){ return nil, errors.New("Malformed question: Choice text is too long.") } choiceIsAllowed := allowedText.VerifyStringIsAllowed(choiceString) if (choiceIsAllowed == false){ return nil, errors.New("Malformed question: Choice contains not-allowed text.") } _, exists := choicesMap[choiceString] if (exists == true){ return nil, errors.New("Malformed question: Duplicate choice exists.") } choicesMap[choiceString] = struct{}{} } } else { // questionType == "Entry" if (questionOptions != "Numeric" && questionOptions != "Any"){ return nil, errors.New("Malformed question: Invalid entry options.") } } newQuestionObject := QuestionObject{ Identifier: questionIdentifier, Type: questionType, Content: questionContent, Options: questionOptions, } questionnaireQuestionsList = append(questionnaireQuestionsList, newQuestionObject) } return questionnaireQuestionsList, nil } func CreateQuestionnaireString(inputQuestionnaireObject []QuestionObject)(string, error){ if (len(inputQuestionnaireObject) == 0){ return "", errors.New("CreateQuestionnaireString called with empty inputQuestionnaireObject.") } if (len(inputQuestionnaireObject) > 25){ return "", errors.New("Cannot create questionnaire: More than 25 questions.") } questionsList := make([]string, 0, len(inputQuestionnaireObject)) for _, questionObject := range inputQuestionnaireObject{ questionIdentifier := questionObject.Identifier questionType := questionObject.Type questionContent := questionObject.Content questionOptions := questionObject.Options questionSlice := []string{questionIdentifier, questionType, questionContent, questionOptions} questionString := strings.Join(questionSlice, "%¢") questionsList = append(questionsList, questionString) } questionnaireString := strings.Join(questionsList, "+&") // Now we verify questionnaire: _, err := ReadQuestionnaireString(questionnaireString) if (err != nil){ return "", errors.New("Cannot create questionnaire: Result questionnaire is invalid: " + err.Error()) } return questionnaireString, nil } //Outputs: // -map[string]string: Response map (Question identifier -> Encoded question response) // -error func ReadQuestionnaireResponse(inputResponseString string)(map[string]string, error){ questionsList := strings.Split(inputResponseString, "+&") if (len(questionsList) > 25){ return nil, errors.New("Invalid questionnaire response: More than 25 questions.") } responseMap := make(map[string]string) for _, element := range questionsList{ questionIdentifier, questionResponse, delimiterFound := strings.Cut(element, "%") if (delimiterFound == false){ return nil, errors.New("Invalid questionnaire response: Invalid encoded response") } questionIdentifierBytes, err := encoding.DecodeHexStringToBytes(questionIdentifier) if (err != nil){ return nil, errors.New("Invalid questionnaire response: Question identifier is not hex: " + questionIdentifier) } if (len(questionIdentifierBytes) != 9 && len(questionIdentifierBytes) != 10){ return nil, errors.New("Invalid questionnaire response: Question identifier is invalid length.") } if (len(questionIdentifierBytes) == 10){ // Question is of type Entry if (questionResponse == ""){ return nil, errors.New("Invalid questionnaire response: Entry response is empty.") } if (len(questionResponse) > 2000){ return nil, errors.New("Invalid questionnaire response: Response too long.") } } else { // Question is of type Choice responseChoiceIndexesList := strings.Split(questionResponse, "$") if (len(responseChoiceIndexesList) > 6){ return nil, errors.New("Invalid questionnaire response: Too many choices.") } // We use a map to prevent duplicates choicesMap := make(map[string]struct{}) for _, responseChoiceIndex := range responseChoiceIndexesList{ responseChoiceIndexInt, err := helpers.ConvertStringToInt(responseChoiceIndex) if (err != nil) { return nil, errors.New("Invalid questionnaire response: Choice index is not an integer.") } if (responseChoiceIndexInt < 1 || responseChoiceIndexInt > 6){ return nil, errors.New("Invalid questionnaire response: Choice is an invalid int") } _, exists := choicesMap[responseChoiceIndex] if (exists == true){ return nil, errors.New("Invalid questionnaire response: Duplicate choice exists.") } choicesMap[responseChoiceIndex] = struct{}{} } } responseMap[questionIdentifier] = questionResponse } return responseMap, nil } //Inputs: // -map[string]string: Response map (Question identifier -> Encoded question response) //Outputs: // -string: Questionnaire response // -error func CreateQuestionnaireResponse(inputResponseMap map[string]string)(string, error){ questionsItemsList := make([]string, 0, len(inputResponseMap)) for questionIdentifier, questionEncodedResponse := range inputResponseMap{ encodedQuestionItem := questionIdentifier + "%" + questionEncodedResponse questionsItemsList = append(questionsItemsList, encodedQuestionItem) } questionnaireResponseString := strings.Join(questionsItemsList, "+&") _, err := ReadQuestionnaireResponse(questionnaireResponseString) if (err != nil) { return "", errors.New("Cannot create questionnaire response: Result is invalid: " + err.Error()) } return questionnaireResponseString, nil }