diff --git a/svc/pkg/domain/model/question/checkbox.go b/svc/pkg/domain/model/question/checkbox.go index 2a0be4d..2a337ad 100644 --- a/svc/pkg/domain/model/question/checkbox.go +++ b/svc/pkg/domain/model/question/checkbox.go @@ -93,7 +93,7 @@ func ImportCheckBoxQuestion(q StandardQuestion) (*CheckBoxQuestion, error) { return NewCheckBoxQuestion(q.ID, q.Text, options, optionsOrder, q.FormID), nil } -func (q CheckBoxQuestion) Export() StandardQuestion { +func (q CheckBoxQuestion) Export() (*StandardQuestion, error) { customs := make(map[string]interface{}) options := make(map[string]string, len(q.Options)) for _, option := range q.Options { @@ -105,7 +105,7 @@ func (q CheckBoxQuestion) Export() StandardQuestion { } customs[CheckBoxOptionsField] = options customs[CheckBoxOptionsOrderField] = optionsOrder - return NewStandardQuestion(TypeCheckBox, q.ID, q.FormID, q.Text, customs) + return NewStandardQuestion(TypeCheckBox, q.ID, q.FormID, q.Text, customs), nil } func (o CheckboxOptionsOrder) GetOrderedIDs() []CheckBoxOptionID { diff --git a/svc/pkg/domain/model/question/file.go b/svc/pkg/domain/model/question/file.go index a297ffe..7a05698 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -9,100 +9,85 @@ import ( type ( FileQuestion struct { Basic - FileType FileType - Constraint FileConstraint + FileTypes FileTypes + ImageFileConstraint + } + FileTypes struct { + AcceptAny bool + AcceptImage bool + AcceptPDF bool } - FileType int ) const ( - Image FileType = 1 - PDF FileType = 2 - Any FileType = 3 - FileQuestionFileTypeField = "fileType" - FileConstraintsCustomsField = "fileConstraint" + FileQuestionFileTypeField = "fileTypes" + FileImageConstraintField = "img_c" ) -func (t FileType) String() string { - switch t { - case Image: - return "image" - case PDF: - return "pdf" - case Any: - return "any" - default: - return "unknown" - } -} - func NewFileQuestion( - id id.QuestionID, text string, fileType FileType, constraint FileConstraint, formID id.FormID, + id id.QuestionID, text string, fileTypes FileTypes, + imgConstraint ImageFileConstraint, + formID id.FormID, ) *FileQuestion { return &FileQuestion{ - Basic: NewBasic(id, text, TypeFile, formID), - FileType: fileType, - Constraint: constraint, - } -} - -func NewFileType(v int) (FileType, error) { - switch FileType(v) { - case Image, PDF, Any: - return FileType(v), nil + Basic: NewBasic(id, text, TypeFile, formID), + FileTypes: fileTypes, + ImageFileConstraint: imgConstraint, } - return 0, errors.New("invalid file type") } func ImportFileQuestion(q StandardQuestion) (*FileQuestion, error) { - // check if customs has "fileType" as int, return error if not + // expect custom field has fileTypes as []bool + // [AcceptAny, AcceptImage, AcceptPDF] fileTypeDataI, has := q.Customs[FileQuestionFileTypeField] if !has { return nil, errors.New( fmt.Sprintf("\"%s\" is required for FileQuestion", FileQuestionFileTypeField)) } - fileTypeData, ok := fileTypeDataI.(int64) + fileTypeData, ok := fileTypeDataI.([]bool) if !ok { return nil, errors.New( fmt.Sprintf("\"%s\" must be int for FileQuestion", FileQuestionFileTypeField)) } - fileType, err := NewFileType(int(fileTypeData)) - if err != nil { - return nil, err + + // extend length if not enough + if len(fileTypeData) < 3 { + for i := len(fileTypeData); i < 3; i++ { + fileTypeData = append(fileTypeData, false) + } } - if fileType == Any { - return NewFileQuestion(q.ID, q.Text, fileType, nil, q.FormID), nil + fileTypes := FileTypes{ + AcceptAny: fileTypeData[0], + AcceptImage: fileTypeData[1], + AcceptPDF: fileTypeData[2], } - constraintsCustomsData, has := q.Customs[FileConstraintsCustomsField] - // if FileConstraintsCustomsField is not present, return FileQuestion without constraint + imgConstraintCustomR, has := q.Customs[FileImageConstraintField] + //if FileConstraintsCustomsField is not present, return FileQuestion without constraint if !has { - return NewFileQuestion(q.ID, q.Text, fileType, nil, q.FormID), nil + return NewFileQuestion(q.ID, q.Text, fileTypes, ImageFileConstraint{}, q.FormID), nil } - constraintsCustoms, ok := constraintsCustomsData.(map[string]interface{}) - // if FileConstraintsCustomsField Found, but it is not map[string]interface{}, return error + imgConstraintCustom, ok := imgConstraintCustomR.(map[string]interface{}) + //if FileConstraintsCustomsField Found, but it is not slice, return error if !ok { return nil, errors.New( - fmt.Sprintf("\"%s\" must be map[string]interface{} for FileQuestion", FileConstraintsCustomsField)) + fmt.Sprintf("\"%s\" must be map[string]interface{} for FileQuestion", FileImageConstraintField)) } - - constraint := NewStandardFileConstraint(fileType, constraintsCustoms) - question := NewFileQuestion(q.ID, q.Text, fileType, ImportFileConstraint(constraint), q.FormID) + imgConstraint, err := ImportImageFileConstraint(imgConstraintCustom) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to import ImageFileConstraint: %w", err) } + question := NewFileQuestion(q.ID, q.Text, fileTypes, *imgConstraint, q.FormID) return question, nil } -func (q FileQuestion) Export() StandardQuestion { +func (q FileQuestion) Export() (*StandardQuestion, error) { customs := make(map[string]interface{}) - customs[FileQuestionFileTypeField] = q.FileType - - if q.Constraint != nil { - customs[FileConstraintsCustomsField] = q.Constraint.Export().Customs - } - return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs) + qt := []bool{q.FileTypes.AcceptAny, q.FileTypes.AcceptImage, q.FileTypes.AcceptPDF} + customs[FileQuestionFileTypeField] = qt + customs[FileImageConstraintField] = q.ImageFileConstraint.Export() + return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs), nil } diff --git a/svc/pkg/domain/model/question/file_constraint.go b/svc/pkg/domain/model/question/file_constraint.go index c2135b8..2b91f53 100644 --- a/svc/pkg/domain/model/question/file_constraint.go +++ b/svc/pkg/domain/model/question/file_constraint.go @@ -1,6 +1,7 @@ package question type ( + FileType int StandardFileConstraint struct { Type FileType Customs map[string]interface{} @@ -8,7 +9,7 @@ type ( FileConstraint interface { GetFileType() FileType GetExtensions() []Extension - Export() StandardFileConstraint + Export() (*StandardFileConstraint, error) ValidateFiles(file []File) error } File struct { @@ -18,18 +19,8 @@ type ( Extension string ) -func NewStandardFileConstraint(fileType FileType, customs map[string]interface{}) StandardFileConstraint { - return StandardFileConstraint{ - Type: fileType, - Customs: customs, - } -} - -func ImportFileConstraint(standard StandardFileConstraint) FileConstraint { - switch standard.Type { - case Image: - return ImportImageFileConstraint(standard) - default: - return nil - } -} +const ( + Image FileType = 1 + PDF FileType = 2 + FileTypeCustomField = "type" +) diff --git a/svc/pkg/domain/model/question/file_constraint_image.go b/svc/pkg/domain/model/question/file_constraint_image.go index 9901bb4..7286747 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -10,17 +10,13 @@ import ( type ( ImageFileConstraint struct { - Ratio float64 - MinNumber int - MaxNumber int - MinResolutionWidth int - MaxResolutionWidth int - MinResolutionHeight int - MaxResolutionHeight int - Extensions []Extension - PNGInfo image.InfoExtractor - JPGInfo image.InfoExtractor - WEBPInfo image.InfoExtractor + Ratio RatioSpec + MinNumber *int + MaxNumber *int + Width, Height DimensionSpec + PNGInfo image.InfoExtractor + JPGInfo image.InfoExtractor + WEBPInfo image.InfoExtractor } ImageType int ) @@ -29,56 +25,35 @@ const ( PNG ImageType = 1 JPG ImageType = 2 WEBP ImageType = 3 + TIFF ImageType = 4 + HEIC ImageType = 5 + SVG ImageType = 6 ) func NewImageFileConstraint( - ratio float64, minNumber, maxNumber, minWidth, maxWidth, minHeight, maxHeight int, extensions []Extension, -) ImageFileConstraint { - return ImageFileConstraint{ - Ratio: ratio, - MinNumber: minNumber, - MaxNumber: maxNumber, - MinResolutionWidth: minWidth, - MaxResolutionWidth: maxWidth, - MinResolutionHeight: minHeight, - MaxResolutionHeight: maxHeight, - Extensions: extensions, - PNGInfo: imagePkg.NewPNGInfo(), - JPGInfo: imagePkg.NewJPEGInfo(), - WEBPInfo: imagePkg.NewWEBPInfo(), + minNumber, maxNumber *int, + width, height DimensionSpec, + ratio RatioSpec, +) (*ImageFileConstraint, error) { + if err := width.Validate(); err != nil { + return nil, fmt.Errorf("invalid width: %w", err) } -} - -func ImportImageFileConstraint(standard StandardFileConstraint) ImageFileConstraint { - ratio, _ := standard.Customs["ratio"].(float64) - minNumber, _ := standard.Customs["minNumber"].(int64) - maxNumber, _ := standard.Customs["maxNumber"].(int64) - minWidth, _ := standard.Customs["minWidth"].(int64) - maxWidth, _ := standard.Customs["maxWidth"].(int64) - minHeight, _ := standard.Customs["minHeight"].(int64) - maxHeight, _ := standard.Customs["maxHeight"].(int64) - extsI, _ := standard.Customs["extensions"].([]interface{}) - exts := make([]Extension, len(extsI)) - for i, extI := range extsI { - exts[i] = extI.(Extension) - } - - return NewImageFileConstraint( - ratio, int(minNumber), int(maxNumber), int(minWidth), int(maxWidth), int(minHeight), int(maxHeight), exts) -} - -func (c ImageFileConstraint) Export() StandardFileConstraint { - return NewStandardFileConstraint(Image, - map[string]interface{}{ - "ratio": c.Ratio, - "minNumber": c.MinNumber, - "maxNumber": c.MaxNumber, - "minWidth": c.MinResolutionWidth, - "maxWidth": c.MaxResolutionWidth, - "minHeight": c.MinResolutionHeight, - "maxHeight": c.MaxResolutionHeight, - "extensions": c.Extensions, - }) + if err := height.Validate(); err != nil { + return nil, fmt.Errorf("invalid height: %w", err) + } + if err := ratio.Validate(); err != nil { + return nil, fmt.Errorf("invalid ratio: %w", err) + } + return &ImageFileConstraint{ + Ratio: ratio, + MinNumber: minNumber, + MaxNumber: maxNumber, + Width: width, + Height: height, + PNGInfo: imagePkg.NewPNGInfo(), + JPGInfo: imagePkg.NewJPEGInfo(), + WEBPInfo: imagePkg.NewWEBPInfo(), + }, nil } func (c ImageFileConstraint) GetFileType() FileType { @@ -86,17 +61,20 @@ func (c ImageFileConstraint) GetFileType() FileType { } func (c ImageFileConstraint) GetExtensions() []Extension { - return c.Extensions + return []Extension{".jpg", ".jpeg", ".png", ".webp"} } func (c ImageFileConstraint) ValidateFiles(files []File) error { - if c.MinNumber > 0 && len(files) < c.MinNumber { + if len(files) == 0 { + return errors.New("no file found") + } + if c.MinNumber != nil && *c.MinNumber <= len(files) { return errors.New(fmt.Sprintf( - "number of files not satisfied. min number: %d, actual number: %d", c.MinNumber, len(files))) + "number of files not satisfied. min number: %d, actual number: %d", *c.MinNumber, len(files))) } - if c.MaxNumber > 0 && len(files) > c.MaxNumber { + if c.MaxNumber != nil && *c.MaxNumber >= len(files) { return errors.New(fmt.Sprintf( - "number of files not satisfied. max number: %d, actual number: %d", c.MaxNumber, len(files))) + "number of files not satisfied. max number: %d, actual number: %d", *c.MaxNumber, len(files))) } for _, file := range files { @@ -131,43 +109,61 @@ func (c ImageFileConstraint) validateProperties(imgType ImageType, file []byte) if err != nil { return err } - if c.MinResolutionWidth > 0 && width < c.MinResolutionWidth { + if width <= 0 || height <= 0 { + return errors.New("invalid image size") + } + if err := validateDimension(c.Width, width, "width"); err != nil { + return err + } + if err := validateDimension(c.Height, height, "height"); err != nil { + return err + } + if err := validateRatio(c.Ratio, float32(width)/float32(height), "ratio"); err != nil { + return err + } + return nil +} + +func validateDimension(d DimensionSpec, value int, name string) error { + if d.Min != nil && value < *d.Min { + return errors.New( + fmt.Sprintf("%s not satisfied. min %s: %d, actual %s: %d", name, name, *d.Min, name, value)) + } + if d.Max != nil && value > *d.Max { return errors.New( - fmt.Sprintf("width not satisfied. min width: %d, actual width: %d", c.MinResolutionWidth, width)) + fmt.Sprintf("%s not satisfied. max %s: %d, actual %s: %d", name, name, *d.Max, name, value)) } - if c.MaxResolutionWidth > 0 && width > c.MaxResolutionWidth { + if d.Eq != nil && value != *d.Eq { return errors.New( - fmt.Sprintf("width not satisfied. max width: %d, actual width: %d", c.MaxResolutionWidth, width)) + fmt.Sprintf("%s not satisfied. expected %s: %d, actual %s: %d", name, name, *d.Eq, name, value)) } - if c.MinResolutionHeight > 0 && height < c.MinResolutionHeight { + return nil +} + +func validateRatio(r RatioSpec, value float32, name string) error { + if r.Min != nil && value < *r.Min { return errors.New( - fmt.Sprintf("height not satisfied. min height: %d, actual height: %d", c.MinResolutionHeight, height)) + fmt.Sprintf("%s not satisfied. min %s: %f, actual %s: %f", name, name, *r.Min, name, value)) } - if c.MaxResolutionHeight > 0 && height > c.MaxResolutionHeight { + if r.Max != nil && value > *r.Max { return errors.New( - fmt.Sprintf("height not satisfied. max height: %d, actual height: %d", c.MaxResolutionHeight, height)) + fmt.Sprintf("%s not satisfied. max %s: %f, actual %s: %f", name, name, *r.Max, name, value)) } - if c.Ratio > 0 { - if ratio := float64(width) / float64(height); ratio != c.Ratio { - return errors.New( - fmt.Sprintf("ratio not satisfied. expected ratio: %f, actual ratio: %f", c.Ratio, ratio)) - } + if r.Eq != nil && value != *r.Eq { + return errors.New( + fmt.Sprintf("%s not satisfied. expected %s: %f, actual %s: %f", name, name, *r.Eq, name, value)) } return nil } func (c ImageFileConstraint) checkExtension(ext string) (ImageType, error) { - if len(c.Extensions) == 0 { - // if extension is not specified, check with default extensions - return convertToImageType(ext) - } - for _, e := range c.Extensions { + for _, e := range c.GetExtensions() { if string(e) == ext { return convertToImageType(ext) } } return 0, errors.New( - fmt.Sprintf("invalid file type. specified extensions: %v", c.Extensions)) + fmt.Sprintf("invalid file type. specified extensions: %v", c.GetExtensions())) } func convertToImageType(ext string) (ImageType, error) { @@ -184,3 +180,112 @@ func convertToImageType(ext string) (ImageType, error) { []string{".jpg", ".jpeg", ".png", ".webp"})) } } + +func (s RatioSpec) Validate() error { + if s.Eq != nil { + if *s.Eq <= 0 { + return errors.New("eq ratio must be positive") + } + if s.Min != nil || s.Max != nil { + return errors.New("eq ratio is specified with min or max ratio") + } + return nil + } + + if s.Min != nil && *s.Min <= 0 { + return errors.New("min ratio must be positive") + } + if s.Max != nil && *s.Max <= 0 { + return errors.New("max ratio must be positive") + } + if s.Min != nil && s.Max != nil && *s.Min > *s.Max { + return errors.New("min ratio is greater than max ratio") + } + return nil +} + +func (s DimensionSpec) Validate() error { + if s.Eq != nil { + if *s.Eq <= 0 { + return errors.New("eq dimension must be positive") + } + if s.Min != nil || s.Max != nil { + return errors.New("eq dimension is specified with min or max dimension") + } + return nil + } + if s.Min != nil && *s.Min <= 0 { + return errors.New("min dimension must be positive") + } + if s.Max != nil && *s.Max <= 0 { + return errors.New("max dimension must be positive") + } + return nil +} + +const ( + FileImageConstraintWidth = "w" + FileImageConstraintHeight = "h" + FileImageConstraintRatio = "r" + FileImageConstraintMinNumber = "min" + FileImageConstraintMaxNumber = "max" + FileImageConstraintDimensionEq = "eq" + FileImageConstraintDimensionMin = "min" + FileImageConstraintDimensionMax = "max" + FileImageConstraintRatioEq = "eq" + FileImageConstraintRatioMin = "min" + FileImageConstraintRatioMax = "max" +) + +func ImportImageFileConstraint(c map[string]interface{}) (*ImageFileConstraint, error) { + width, err := loadDimensionSpec(c, FileImageConstraintWidth) + if err != nil { + return nil, fmt.Errorf("failed to load width: %w", err) + } + height, err := loadDimensionSpec(c, FileImageConstraintHeight) + if err != nil { + return nil, fmt.Errorf("failed to load height: %w", err) + } + ratio, err := loadRatioSpec(c, FileImageConstraintRatio) + if err != nil { + return nil, fmt.Errorf("failed to load ratio: %w", err) + } + minNumber, err := loadInt(c, FileImageConstraintMinNumber) + if err != nil { + return nil, fmt.Errorf("failed to load min number: %w", err) + } + maxNumber, err := loadInt(c, FileImageConstraintMaxNumber) + if err != nil { + return nil, fmt.Errorf("failed to load max number: %w", err) + } + return NewImageFileConstraint(minNumber, maxNumber, width, height, ratio) +} + +func loadInt(t map[string]interface{}, key string) (*int, error) { + v, has := t[key] + if !has { + return nil, nil + } + i, ok := v.(int) + if !ok { + return nil, errors.New(fmt.Sprintf("invalid %s", key)) + } + return &i, nil +} + +func (c ImageFileConstraint) Export() map[string]interface{} { + result := map[string]interface{}{} + if c.MinNumber != nil { + result[FileImageConstraintMinNumber] = *c.MinNumber + } + if c.MaxNumber != nil { + result[FileImageConstraintMaxNumber] = *c.MaxNumber + } + widthC := c.Width.Export() + result[FileImageConstraintWidth] = widthC + heightC := c.Height.Export() + result[FileImageConstraintHeight] = heightC + ratioC := c.Ratio.Export() + result[FileImageConstraintRatio] = ratioC + return result +} diff --git a/svc/pkg/domain/model/question/file_constraint_image_spec.go b/svc/pkg/domain/model/question/file_constraint_image_spec.go new file mode 100644 index 0000000..9b09741 --- /dev/null +++ b/svc/pkg/domain/model/question/file_constraint_image_spec.go @@ -0,0 +1,130 @@ +package question + +import "errors" + +type ( + DimensionSpec struct { + Min, Max *int + Eq *int + } + + // RatioSpec represents the ratio of width / height + RatioSpec struct { + Min, Max *float32 + Eq *float32 + } +) + +func NewDimensionSpec(min, max, eq *int) DimensionSpec { + if eq != nil { + return DimensionSpec{Eq: eq} + } + return DimensionSpec{Min: min, Max: max, Eq: nil} +} + +func NewRatioSpec(min, max, eq *float32) RatioSpec { + if eq != nil { + return RatioSpec{Eq: eq} + } + return RatioSpec{Min: min, Max: max, Eq: nil} +} + +func (s DimensionSpec) Export() map[string]interface{} { + result := map[string]interface{}{} + if s.Eq != nil { + result[FileImageConstraintDimensionEq] = *s.Eq + } else { + if s.Min != nil { + result[FileImageConstraintDimensionMin] = *s.Min + } + if s.Max != nil { + result[FileImageConstraintDimensionMax] = *s.Max + } + } + return result +} + +func (s RatioSpec) Export() map[string]interface{} { + result := map[string]interface{}{} + if s.Eq != nil { + result[FileImageConstraintRatioEq] = *s.Eq + } else { + if s.Min != nil { + result[FileImageConstraintRatioMin] = *s.Min + } + if s.Max != nil { + result[FileImageConstraintRatioMax] = *s.Max + } + } + return result +} + +func loadDimensionSpec(c map[string]interface{}, key string) (DimensionSpec, error) { + target := DimensionSpec{} + t, has := c[key] + if !has { + return DimensionSpec{}, nil + } + m, ok := t.(map[string]interface{}) + if !ok { + return DimensionSpec{}, errors.New("invalid dimension spec") + } + if eqR, has := m[FileImageConstraintDimensionEq]; has && eqR != nil { + eqV, ok := eqR.(int) + if !ok { + return DimensionSpec{}, errors.New("invalid eq dimension") + } + target.Eq = &eqV + } else { + if minR, has := m[FileImageConstraintDimensionMin]; has && minR != nil { + minV, ok := minR.(int) + if !ok { + return DimensionSpec{}, errors.New("invalid min dimension") + } + target.Min = &minV + } + if maxR, has := m[FileImageConstraintDimensionMax]; has && maxR != nil { + maxV, ok := maxR.(int) + if !ok { + return DimensionSpec{}, errors.New("invalid max dimension") + } + target.Max = &maxV + } + } + return target, nil +} + +func loadRatioSpec(c map[string]interface{}, key string) (RatioSpec, error) { + target := RatioSpec{} + t, has := c[key] + if !has { + return RatioSpec{}, nil + } + m, ok := t.(map[string]interface{}) + if !ok { + return RatioSpec{}, errors.New("invalid ratio spec") + } + if eqR, has := m[FileImageConstraintRatioEq]; has && eqR != nil { + eqV, ok := eqR.(float32) + if !ok { + return RatioSpec{}, errors.New("invalid eq ratio") + } + target.Eq = &eqV + } else { + if minR, has := m[FileImageConstraintRatioMin]; has && minR != nil { + minV, ok := minR.(float32) + if !ok { + return RatioSpec{}, errors.New("invalid min ratio") + } + target.Min = &minV + } + if maxR, has := m[FileImageConstraintRatioMax]; has && maxR != nil { + maxV, ok := maxR.(float32) + if !ok { + return RatioSpec{}, errors.New("invalid max ratio") + } + target.Max = &maxV + } + } + return target, nil +} diff --git a/svc/pkg/domain/model/question/file_test.go b/svc/pkg/domain/model/question/file_test.go new file mode 100644 index 0000000..cbb9d2c --- /dev/null +++ b/svc/pkg/domain/model/question/file_test.go @@ -0,0 +1,169 @@ +package question + +import ( + "github.com/stretchr/testify/assert" + "testing" + "ynufes-mypage-backend/pkg/identity" +) + +func TestImportFileQuestion(t *testing.T) { + f1, f2, f3 := float32(0.5), float32(1.5), float32(3.0) + v1, v2, v3 := 100, 200, 300 + n1, n2 := 3, 5 + SampleID1, sampleID2 := identity.IssueID(), identity.IssueID() + tests := []struct { + name string + from StandardQuestion + want *FileQuestion + }{ + { + name: "ImageQuestion - Simple", + from: StandardQuestion{ + ID: SampleID1, + Text: "Image Question", + FormID: sampleID2, + Type: TypeFile, + Customs: map[string]interface{}{ + FileQuestionFileTypeField: []bool{false, true, false}, + FileImageConstraintField: map[string]interface{}{ + FileImageConstraintRatio: map[string]interface{}{}, + FileImageConstraintWidth: map[string]interface{}{}, + FileImageConstraintHeight: map[string]interface{}{}, + }, + }, + }, + want: NewFileQuestion(SampleID1, + "Image Question", + FileTypes{ + AcceptAny: false, + AcceptImage: true, + AcceptPDF: false, + }, ImageFileConstraint{}, sampleID2), + }, + { + name: "ImageQuestion - With Constraints", + from: StandardQuestion{ + ID: SampleID1, + Text: "Image Question", + FormID: sampleID2, + Type: TypeFile, + Customs: map[string]interface{}{ + FileQuestionFileTypeField: []bool{false, true, false}, + FileImageConstraintField: map[string]interface{}{ + FileImageConstraintRatio: map[string]interface{}{ + FileImageConstraintRatioEq: f1, + }, + FileImageConstraintWidth: map[string]interface{}{ + FileImageConstraintDimensionMin: v1, + FileImageConstraintDimensionMax: v2, + }, + FileImageConstraintHeight: map[string]interface{}{ + FileImageConstraintDimensionMin: v2, + FileImageConstraintDimensionMax: v3, + }, + }, + }, + }, + want: NewFileQuestion(SampleID1, + "Image Question", + FileTypes{ + AcceptAny: false, + AcceptImage: true, + AcceptPDF: false, + }, ImageFileConstraint{ + Ratio: NewRatioSpec(nil, nil, &f1), + MinNumber: nil, + MaxNumber: nil, + Width: NewDimensionSpec(&v1, &v2, nil), + Height: NewDimensionSpec(&v2, &v3, nil), + }, sampleID2), + }, + { + name: "ImageQuestion - With Constraints - MinMax", + from: StandardQuestion{ + ID: SampleID1, + Text: "Image Question", + FormID: sampleID2, + Type: TypeFile, + Customs: map[string]interface{}{ + FileQuestionFileTypeField: []bool{false, true, false}, + FileImageConstraintField: map[string]interface{}{ + FileImageConstraintRatio: map[string]interface{}{ + FileImageConstraintRatioMin: f2, + FileImageConstraintRatioMax: f3, + }, + FileImageConstraintWidth: map[string]interface{}{ + FileImageConstraintRatioEq: v1, + }, + FileImageConstraintHeight: map[string]interface{}{ + FileImageConstraintRatioEq: v2, + }, + }, + }, + }, + want: NewFileQuestion(SampleID1, + "Image Question", + FileTypes{ + AcceptAny: false, + AcceptImage: true, + AcceptPDF: false, + }, ImageFileConstraint{ + Ratio: NewRatioSpec(&f2, &f3, nil), + MinNumber: nil, + MaxNumber: nil, + Width: NewDimensionSpec(nil, nil, &v1), + Height: NewDimensionSpec(nil, nil, &v2), + }, sampleID2), + }, + { + name: "ImageQuestion - With Constraints - MinMaxNumber", + from: StandardQuestion{ + ID: SampleID1, + Text: "Image Question", + FormID: sampleID2, + Type: TypeFile, + Customs: map[string]interface{}{ + FileQuestionFileTypeField: []bool{false, true, false}, + FileImageConstraintField: map[string]interface{}{ + FileImageConstraintMinNumber: n1, + FileImageConstraintMaxNumber: n2, + FileImageConstraintWidth: map[string]interface{}{}, + FileImageConstraintHeight: map[string]interface{}{}, + FileImageConstraintRatio: map[string]interface{}{}, + }, + }, + }, + want: NewFileQuestion(SampleID1, + "Image Question", + FileTypes{ + AcceptAny: false, + AcceptImage: true, + AcceptPDF: false, + }, + ImageFileConstraint{ + Ratio: NewRatioSpec(nil, nil, nil), + MinNumber: &n1, + MaxNumber: &n2, + Width: NewDimensionSpec(nil, nil, nil), + Height: NewDimensionSpec(nil, nil, nil), + }, sampleID2), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ImportFileQuestion(tc.from) + assert.NoError(t, err) + assert.Equal(t, tc.want.ID, got.ID) + assert.Equal(t, tc.want.Text, got.Text) + assert.Equal(t, tc.want.MinNumber, got.MinNumber) + assert.Equal(t, tc.want.MaxNumber, got.MaxNumber) + assert.Equal(t, tc.want.Width, got.Width) + assert.Equal(t, tc.want.Height, got.Height) + assert.Equal(t, tc.want.Ratio, got.Ratio) + export, err := got.Export() + if assert.NoError(t, err) { + assert.Equal(t, tc.from, *export) + } + }) + } +} diff --git a/svc/pkg/domain/model/question/question.go b/svc/pkg/domain/model/question/question.go index e2c4f83..77b701d 100644 --- a/svc/pkg/domain/model/question/question.go +++ b/svc/pkg/domain/model/question/question.go @@ -8,7 +8,7 @@ import ( type ( Type int Question interface { - Export() StandardQuestion + Export() (*StandardQuestion, error) GetType() Type AssignID(id.QuestionID) error GetID() id.QuestionID @@ -59,8 +59,8 @@ func NewType(t string) (Type, error) { func NewStandardQuestion( t Type, id id.QuestionID, formID id.FormID, text string, customs map[string]interface{}, -) StandardQuestion { - return StandardQuestion{ +) *StandardQuestion { + return &StandardQuestion{ ID: id, Text: text, FormID: formID, diff --git a/svc/pkg/domain/model/question/radio_button.go b/svc/pkg/domain/model/question/radio_button.go index 0b85c0a..906dcc7 100644 --- a/svc/pkg/domain/model/question/radio_button.go +++ b/svc/pkg/domain/model/question/radio_button.go @@ -87,7 +87,7 @@ func ImportRadioButtonsQuestion(q StandardQuestion) (*RadioButtonsQuestion, erro ), nil } -func (q RadioButtonsQuestion) Export() StandardQuestion { +func (q RadioButtonsQuestion) Export() (*StandardQuestion, error) { customs := make(map[string]interface{}) options := make(map[string]string, len(q.Options)) @@ -103,7 +103,7 @@ func (q RadioButtonsQuestion) Export() StandardQuestion { customs[RadioButtonOptionsField] = options customs[RadioButtonOptionsOrderField] = optionsOrder - return NewStandardQuestion(TypeRadio, q.ID, q.FormID, q.Text, customs) + return NewStandardQuestion(TypeRadio, q.ID, q.FormID, q.Text, customs), nil } func (o RadioButtonOptionsOrder) GetOrderedIDs() []RadioButtonOptionID { diff --git a/svc/pkg/handler/agent/question.go b/svc/pkg/handler/agent/question.go index e165c42..599a79f 100644 --- a/svc/pkg/handler/agent/question.go +++ b/svc/pkg/handler/agent/question.go @@ -89,7 +89,12 @@ func (q Question) CreateHandler() gin.HandlerFunc { var checkbox *schemaQ.CheckboxQuestionInfo switch qType { case question.TypeCheckBox: - checkQ, err := question.ImportCheckBoxQuestion(res.Question.Export()) + st, err := res.Question.Export() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + checkQ, err := question.ImportCheckBoxQuestion(*st) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -106,7 +111,12 @@ func (q Question) CreateHandler() gin.HandlerFunc { Options: opts, } case question.TypeRadio: - radioQ, err := question.ImportRadioButtonsQuestion(res.Question.Export()) + st, err := res.Question.Export() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + radioQ, err := question.ImportRadioButtonsQuestion(*st) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return diff --git a/svc/pkg/handler/section/info.go b/svc/pkg/handler/section/info.go index 2dde4e3..5914dbe 100644 --- a/svc/pkg/handler/section/info.go +++ b/svc/pkg/handler/section/info.go @@ -62,7 +62,12 @@ func (h Section) InfoHandler() gin.HandlerFunc { } switch target.GetType() { case question.TypeRadio: - radioQ, err := question.ImportRadioButtonsQuestion(target.Export()) + st, err := target.Export() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + radioQ, err := question.ImportRadioButtonsQuestion(*st) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -77,7 +82,12 @@ func (h Section) InfoHandler() gin.HandlerFunc { } respQ.Options = &options case question.TypeCheckBox: - checkQ, err := question.ImportCheckBoxQuestion(target.Export()) + st, err := target.Export() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + checkQ, err := question.ImportCheckBoxQuestion(*st) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -92,17 +102,27 @@ func (h Section) InfoHandler() gin.HandlerFunc { } respQ.Options = &options case question.TypeFile: - fileQ, err := question.ImportFileQuestion(target.Export()) + st, err := target.Export() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + fileQ, err := question.ImportFileQuestion(*st) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } - exts := make([]string, len(fileQ.Constraint.GetExtensions())) - for i := range fileQ.Constraint.GetExtensions() { - exts[i] = string(fileQ.Constraint.GetExtensions()[i]) + exts := make([]string, len(fileQ.ImageFileConstraint.GetExtensions())) + for i, e := range fileQ.ImageFileConstraint.GetExtensions() { + exts[i] = string(e) + } + ft := schemaS.FileTypes{ + AcceptAny: fileQ.FileTypes.AcceptAny, + AcceptImage: fileQ.FileTypes.AcceptImage, + AcceptPDF: fileQ.FileTypes.AcceptPDF, } fConstraint := schemaS.FileConstraint{ - FileType: fileQ.Constraint.GetFileType().String(), + FileType: ft, Extensions: exts, } respQ.FileConstraint = &fConstraint diff --git a/svc/pkg/infra/reader/question_test.go b/svc/pkg/infra/reader/question_test.go index ecf14d2..08df8f8 100644 --- a/svc/pkg/infra/reader/question_test.go +++ b/svc/pkg/infra/reader/question_test.go @@ -65,7 +65,17 @@ func TestQuestion_GetByID(t *testing.T) { return } if tt.wantErr == nil { - assert.Equal(t, tt.want.Export(), (*got).Export()) + wantE, err := tt.want.Export() + if err != nil { + t.Errorf("GetByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotE, err := (*got).Export() + if err != nil { + t.Errorf("GetByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, wantE, gotE) assert.Equal(t, tt.want.GetID(), (*got).GetID()) assert.Equal(t, tt.want.GetType(), (*got).GetType()) assert.Equal(t, tt.want.GetText(), (*got).GetText()) @@ -162,8 +172,18 @@ func TestQuestion_ListByFormID(t *testing.T) { fmt.Printf("id not equal: %v, %v\n", a.GetID(), b.GetID()) equal = false } - if reflect.DeepEqual(a.Export().Customs, b.Export().Customs) { - fmt.Printf("customs not equal: %v, %v\n", a.Export().Customs, b.Export().Customs) + aE, err := a.Export() + if err != nil { + fmt.Printf("error: %v\n", err) + equal = false + } + bE, err := b.Export() + if err != nil { + fmt.Printf("error: %v\n", err) + equal = false + } + if reflect.DeepEqual(aE, bE) { + fmt.Printf("export not equal: %v, %v\n", aE, bE) equal = false } return equal diff --git a/svc/pkg/infra/writer/question.go b/svc/pkg/infra/writer/question.go index cf246f3..42d3140 100644 --- a/svc/pkg/infra/writer/question.go +++ b/svc/pkg/infra/writer/question.go @@ -26,15 +26,22 @@ func NewQuestion(f *firebase.Firebase) Question { func (w Question) Create(ctx context.Context, q *question.Question) error { newID := identity.IssueID() + if q == nil { + return fmt.Errorf("question is nil") + } if err := (*q).AssignID(newID); err != nil { return err } + st, err := (*q).Export() + if err != nil { + return fmt.Errorf("failed to export question: %w", err) + } e := entity.NewQuestion( (*q).GetID(), (*q).GetFormID().ExportID(), (*q).GetText(), int((*q).GetType()), - (*q).Export().Customs, + st.Customs, ) if err := w.ref.Child((*q).GetID().ExportID()). Set(ctx, e); err != nil { @@ -61,12 +68,16 @@ func (w Question) Set(ctx context.Context, q question.Question) error { if q.GetID() == nil || !q.GetID().HasValue() { return exception.ErrIDNotAssigned } + st, err := q.Export() + if err != nil { + return fmt.Errorf("failed to export question: %w", err) + } e := entity.NewQuestion( q.GetID(), q.GetFormID().ExportID(), q.GetText(), int(q.GetType()), - q.Export().Customs, + st.Customs, ) if err := w.ref.Child(q.GetID().ExportID()). Set(ctx, e); err != nil { diff --git a/svc/pkg/schema/section/info.go b/svc/pkg/schema/section/info.go index ce09242..910f348 100644 --- a/svc/pkg/schema/section/info.go +++ b/svc/pkg/schema/section/info.go @@ -26,6 +26,33 @@ type TextConstraint struct { } type FileConstraint struct { - FileType string `json:"file_type"` - Extensions []string `json:"extensions"` + FileType FileTypes `json:"file_types"` + Extensions []string `json:"extensions"` + ImageConstraint *ImageConstraint `json:"img_constraint,omitempty"` +} + +type FileTypes struct { + AcceptAny bool `json:"any"` + AcceptImage bool `json:"image"` + AcceptPDF bool `json:"pdf"` +} + +type ImageConstraint struct { + Min *int `json:"min"` + Max *int `json:"max"` + Width DimensionSpec `json:"width"` + Height DimensionSpec `json:"height"` + Ratio RatioSpec `json:"ratio"` +} + +type DimensionSpec struct { + Min *int `json:"min"` + Max *int `json:"max"` + Eq *int `json:"eq"` +} + +type RatioSpec struct { + Min *float32 `json:"min"` + Max *float32 `json:"max"` + Eq *float32 `json:"eq"` }