seekia/internal/mateQuestionnaire/mateQuestionnaire.go

310 lines
9.3 KiB
Go

// 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
}