311 lines
9.3 KiB
Go
311 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
|
||
|
}
|
||
|
|
||
|
|
||
|
|