From 0bcdaedcdbee86ead7584e6279142da1c741b991 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 13 May 2024 05:58:02 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20(model/question)FileTypes=20i?= =?UTF-8?q?nstead=20of=20FileType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file.go | 72 +++++++++++---------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/svc/pkg/domain/model/question/file.go b/svc/pkg/domain/model/question/file.go index a297ffe..c0a15e1 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -9,76 +9,62 @@ import ( type ( FileQuestion struct { Basic - FileType FileType + FileTypes FileTypes Constraint FileConstraint } - FileType int + FileTypes struct { + AcceptAny bool + AcceptImage bool + AcceptPDF bool + } ) const ( - Image FileType = 1 - PDF FileType = 2 - Any FileType = 3 - FileQuestionFileTypeField = "fileType" - FileConstraintsCustomsField = "fileConstraint" + FileQuestionFileTypeField = "fileTypes" + FileConstraintsCustomsField = "fileConstraint" ) -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, constraint FileConstraint, formID id.FormID, ) *FileQuestion { return &FileQuestion{ Basic: NewBasic(id, text, TypeFile, formID), - FileType: fileType, + FileTypes: fileTypes, Constraint: constraint, } } -func NewFileType(v int) (FileType, error) { - switch FileType(v) { - case Image, PDF, Any: - return FileType(v), nil - } - 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 if !has { - return NewFileQuestion(q.ID, q.Text, fileType, nil, q.FormID), nil + return NewFileQuestion(q.ID, q.Text, fileTypes, nil, q.FormID), nil } constraintsCustoms, ok := constraintsCustomsData.(map[string]interface{}) @@ -88,18 +74,16 @@ func ImportFileQuestion(q StandardQuestion) (*FileQuestion, error) { fmt.Sprintf("\"%s\" must be map[string]interface{} for FileQuestion", FileConstraintsCustomsField)) } - constraint := NewStandardFileConstraint(fileType, constraintsCustoms) - question := NewFileQuestion(q.ID, q.Text, fileType, ImportFileConstraint(constraint), q.FormID) - if err != nil { - return nil, err - } + constraint := NewStandardFileConstraint(fileTypes, constraintsCustoms) + question := NewFileQuestion(q.ID, q.Text, fileTypes, ImportFileConstraint(constraint), q.FormID) return question, nil } func (q FileQuestion) Export() StandardQuestion { customs := make(map[string]interface{}) - customs[FileQuestionFileTypeField] = q.FileType + qt := []bool{q.FileTypes.AcceptAny, q.FileTypes.AcceptImage, q.FileTypes.AcceptPDF} + customs[FileQuestionFileTypeField] = qt if q.Constraint != nil { customs[FileConstraintsCustomsField] = q.Constraint.Export().Customs From 8d5f0c30af9697fb48de0b552038b0fff3544fa4 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 13 May 2024 07:06:27 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9C=A8=20(model/question)=20FileType?= =?UTF-8?q?=20as=20FileConstraint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file_constraint.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/svc/pkg/domain/model/question/file_constraint.go b/svc/pkg/domain/model/question/file_constraint.go index c2135b8..a6d3db9 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{} @@ -18,6 +19,11 @@ type ( Extension string ) +const ( + Image FileType = 1 + PDF FileType = 2 +) + func NewStandardFileConstraint(fileType FileType, customs map[string]interface{}) StandardFileConstraint { return StandardFileConstraint{ Type: fileType, From a70c3e581796c0a4979080b0b9f7276d0110b822 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 13 May 2024 09:02:42 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(model/question)=20c?= =?UTF-8?q?hange=20signature=20of=20FileConstraint=20Export()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/question/file_constraint.go | 24 +++++++++++-------- .../model/question/file_constraint_image.go | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/svc/pkg/domain/model/question/file_constraint.go b/svc/pkg/domain/model/question/file_constraint.go index a6d3db9..66b1ee3 100644 --- a/svc/pkg/domain/model/question/file_constraint.go +++ b/svc/pkg/domain/model/question/file_constraint.go @@ -1,5 +1,7 @@ package question +import "fmt" + type ( FileType int StandardFileConstraint struct { @@ -9,7 +11,7 @@ type ( FileConstraint interface { GetFileType() FileType GetExtensions() []Extension - Export() StandardFileConstraint + Export() (*StandardFileConstraint, error) ValidateFiles(file []File) error } File struct { @@ -20,22 +22,24 @@ type ( ) const ( - Image FileType = 1 - PDF FileType = 2 + Image FileType = 1 + PDF FileType = 2 + FileTypeCustomField = "type" ) -func NewStandardFileConstraint(fileType FileType, customs map[string]interface{}) StandardFileConstraint { - return StandardFileConstraint{ +func NewStandardFileConstraint(fileType FileType, customs map[string]interface{}) (*StandardFileConstraint, error) { + customs[FileTypeCustomField] = fileType + return &StandardFileConstraint{ Type: fileType, Customs: customs, - } + }, nil } -func ImportFileConstraint(standard StandardFileConstraint) FileConstraint { - switch standard.Type { +func ImportFileConstraint(st StandardFileConstraint) (FileType, FileConstraint, error) { + switch st.Type { case Image: - return ImportImageFileConstraint(standard) + return Image, ImportImageFileConstraint(st), nil default: - return nil + return 0, nil, fmt.Errorf("invalid file 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..9b1a48f 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -67,7 +67,7 @@ func ImportImageFileConstraint(standard StandardFileConstraint) ImageFileConstra ratio, int(minNumber), int(maxNumber), int(minWidth), int(maxWidth), int(minHeight), int(maxHeight), exts) } -func (c ImageFileConstraint) Export() StandardFileConstraint { +func (c ImageFileConstraint) Export() (*StandardFileConstraint, error) { return NewStandardFileConstraint(Image, map[string]interface{}{ "ratio": c.Ratio, From 361eb4371a14c16c3630ae874194a0973c8062ed Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 13 May 2024 11:06:46 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(model/question)=20c?= =?UTF-8?q?hange=20signature=20of=20Question=20Export()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/checkbox.go | 4 ++-- svc/pkg/domain/model/question/file.go | 4 ++-- svc/pkg/domain/model/question/question.go | 6 +++--- svc/pkg/domain/model/question/radio_button.go | 4 ++-- svc/pkg/handler/agent/question.go | 14 ++++++++++++-- svc/pkg/infra/writer/question.go | 15 +++++++++++++-- 6 files changed, 34 insertions(+), 13 deletions(-) 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 c0a15e1..20912f4 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -79,7 +79,7 @@ func ImportFileQuestion(q StandardQuestion) (*FileQuestion, error) { return question, nil } -func (q FileQuestion) Export() StandardQuestion { +func (q FileQuestion) Export() (*StandardQuestion, error) { customs := make(map[string]interface{}) qt := []bool{q.FileTypes.AcceptAny, q.FileTypes.AcceptImage, q.FileTypes.AcceptPDF} @@ -88,5 +88,5 @@ func (q FileQuestion) Export() StandardQuestion { if q.Constraint != nil { customs[FileConstraintsCustomsField] = q.Constraint.Export().Customs } - return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs) + return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs), nil } 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/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 { From 3c68b89051e126faf3739d9248dfeeadb27618f9 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 13 May 2024 11:10:24 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=A8=20(model/question)=20impl=20mul?= =?UTF-8?q?tiple=20FileConstraints=20for=20a=20question?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file.go | 52 ++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/svc/pkg/domain/model/question/file.go b/svc/pkg/domain/model/question/file.go index 20912f4..a2e92c5 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -9,8 +9,8 @@ import ( type ( FileQuestion struct { Basic - FileTypes FileTypes - Constraint FileConstraint + FileTypes FileTypes + Constraints map[FileType]FileConstraint } FileTypes struct { AcceptAny bool @@ -25,12 +25,12 @@ const ( ) func NewFileQuestion( - id id.QuestionID, text string, fileTypes FileTypes, constraint FileConstraint, formID id.FormID, + id id.QuestionID, text string, fileTypes FileTypes, constraint map[FileType]FileConstraint, formID id.FormID, ) *FileQuestion { return &FileQuestion{ - Basic: NewBasic(id, text, TypeFile, formID), - FileTypes: fileTypes, - Constraint: constraint, + Basic: NewBasic(id, text, TypeFile, formID), + FileTypes: fileTypes, + Constraints: constraint, } } @@ -62,20 +62,39 @@ func ImportFileQuestion(q StandardQuestion) (*FileQuestion, error) { } constraintsCustomsData, has := q.Customs[FileConstraintsCustomsField] - // if FileConstraintsCustomsField is not present, return FileQuestion without constraint + //if FileConstraintsCustomsField is not present, return FileQuestion without constraint if !has { return NewFileQuestion(q.ID, q.Text, fileTypes, nil, q.FormID), nil } - constraintsCustoms, ok := constraintsCustomsData.(map[string]interface{}) - // if FileConstraintsCustomsField Found, but it is not map[string]interface{}, return error + constraintsCustoms, ok := constraintsCustomsData.([]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)) } - constraint := NewStandardFileConstraint(fileTypes, constraintsCustoms) - question := NewFileQuestion(q.ID, q.Text, fileTypes, ImportFileConstraint(constraint), q.FormID) + constraints := make(map[FileType]FileConstraint, len(constraintsCustoms)) + + for _, constraintCustomsDataI := range constraintsCustoms { + constraintCustomsData, ok := constraintCustomsDataI.(map[string]interface{}) + if !ok { + return nil, errors.New( + fmt.Sprintf("\"%s\" must be map[string]interface{} for FileQuestion", FileConstraintsCustomsField)) + } + + st, err := NewStandardFileConstraint(FileType(constraintCustomsData[FileTypeCustomField].(float64)), constraintCustomsData) + if err != nil { + return nil, fmt.Errorf("failed to import file constraint: %w", err) + } + fileType, constraint, err := ImportFileConstraint(*st) + if err != nil { + return nil, fmt.Errorf("failed to import file constraint: %w", err) + } + constraints[fileType] = constraint + } + + question := NewFileQuestion(q.ID, q.Text, fileTypes, constraints, q.FormID) return question, nil } @@ -85,8 +104,15 @@ func (q FileQuestion) Export() (*StandardQuestion, error) { qt := []bool{q.FileTypes.AcceptAny, q.FileTypes.AcceptImage, q.FileTypes.AcceptPDF} customs[FileQuestionFileTypeField] = qt - if q.Constraint != nil { - customs[FileConstraintsCustomsField] = q.Constraint.Export().Customs + if q.Constraints != nil { + constraints := make([]map[string]interface{}, 0, len(q.Constraints)) + for _, constraint := range q.Constraints { + c, err := constraint.Export() + if err != nil { + return nil, fmt.Errorf("failed to export file constraint: %w", err) + } + constraints = append(constraints, c.Customs) + } } return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs), nil } From a4280d65964a1006c9dab53602469e61a3946535 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Sun, 26 May 2024 21:38:32 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20(model/question)=20ImageFileC?= =?UTF-8?q?onstraint=20with=20distinctive=20Implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/question/file_constraint.go | 19 -- .../model/question/file_constraint_image.go | 213 +++++++++++------- 2 files changed, 131 insertions(+), 101 deletions(-) diff --git a/svc/pkg/domain/model/question/file_constraint.go b/svc/pkg/domain/model/question/file_constraint.go index 66b1ee3..2b91f53 100644 --- a/svc/pkg/domain/model/question/file_constraint.go +++ b/svc/pkg/domain/model/question/file_constraint.go @@ -1,7 +1,5 @@ package question -import "fmt" - type ( FileType int StandardFileConstraint struct { @@ -26,20 +24,3 @@ const ( PDF FileType = 2 FileTypeCustomField = "type" ) - -func NewStandardFileConstraint(fileType FileType, customs map[string]interface{}) (*StandardFileConstraint, error) { - customs[FileTypeCustomField] = fileType - return &StandardFileConstraint{ - Type: fileType, - Customs: customs, - }, nil -} - -func ImportFileConstraint(st StandardFileConstraint) (FileType, FileConstraint, error) { - switch st.Type { - case Image: - return Image, ImportImageFileConstraint(st), nil - default: - return 0, nil, fmt.Errorf("invalid file type") - } -} diff --git a/svc/pkg/domain/model/question/file_constraint_image.go b/svc/pkg/domain/model/question/file_constraint_image.go index 9b1a48f..28c6e15 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -9,18 +9,25 @@ import ( ) type ( + DimensionSpec struct { + Min, Max *int + Eq *int + } + + // RatioSpec represents the ratio of width / height + RatioSpec struct { + Min, Max *float32 + Eq *float32 + } + 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 +36,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(), - } -} - -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, error) { - 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, - }) + minNumber, maxNumber *int, + width, height DimensionSpec, + ratio RatioSpec, +) (*ImageFileConstraint, error) { + if err := width.Validate(); err != nil { + return nil, fmt.Errorf("invalid width: %w", err) + } + 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 +72,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 +120,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("width not satisfied. min width: %d, actual width: %d", c.MinResolutionWidth, width)) + fmt.Sprintf("%s not satisfied. min %s: %d, actual %s: %d", name, name, *d.Min, name, value)) } - if c.MaxResolutionWidth > 0 && width > c.MaxResolutionWidth { + if d.Max != nil && value > *d.Max { return errors.New( - fmt.Sprintf("width not satisfied. max width: %d, actual width: %d", c.MaxResolutionWidth, width)) + fmt.Sprintf("%s not satisfied. max %s: %d, actual %s: %d", name, name, *d.Max, name, value)) } - if c.MinResolutionHeight > 0 && height < c.MinResolutionHeight { + if d.Eq != nil && value != *d.Eq { return errors.New( - fmt.Sprintf("height not satisfied. min height: %d, actual height: %d", c.MinResolutionHeight, height)) + fmt.Sprintf("%s not satisfied. expected %s: %d, actual %s: %d", name, name, *d.Eq, name, value)) } - if c.MaxResolutionHeight > 0 && height > c.MaxResolutionHeight { + 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. max height: %d, actual height: %d", c.MaxResolutionHeight, height)) + fmt.Sprintf("%s not satisfied. min %s: %f, actual %s: %f", name, name, *r.Min, 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.Max != nil && value > *r.Max { + return errors.New( + fmt.Sprintf("%s not satisfied. max %s: %f, actual %s: %f", name, name, *r.Max, name, value)) + } + 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 +191,45 @@ 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 +} From 8a90b25f25ddd23a308c6dea48ffee8fb946bba6 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Sun, 26 May 2024 22:19:32 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=9C=A8=20(schema/section)=20response?= =?UTF-8?q?=20schema=20for=20ImageConstraint=20FileTypes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/schema/section/info.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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"` } From 48f53d21f4cc2fafeb4f979764d2944f299bfa72 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Sun, 26 May 2024 23:12:07 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=E2=9C=A8=20(schema/section)=20Import&Exp?= =?UTF-8?q?ort=20logic=20for=20ImageFileConstraint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/question/file_constraint_image.go | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/svc/pkg/domain/model/question/file_constraint_image.go b/svc/pkg/domain/model/question/file_constraint_image.go index 28c6e15..b734238 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -233,3 +233,168 @@ func (s DimensionSpec) Validate() error { } 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 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 +} + +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 + return result +} + +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 +} From b30a2979c553d2caaabb2b0ae1f5d0b5ae1bbbf6 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Sun, 26 May 2024 23:22:35 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=92=A5=20(model/question)=20remove?= =?UTF-8?q?=20map[FileType]FileConstraint,=20add=20ImageFileConstraint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file.go | 62 ++++++++------------------- svc/pkg/handler/section/info.go | 34 ++++++++++++--- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/svc/pkg/domain/model/question/file.go b/svc/pkg/domain/model/question/file.go index a2e92c5..5ddee16 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -9,8 +9,8 @@ import ( type ( FileQuestion struct { Basic - FileTypes FileTypes - Constraints map[FileType]FileConstraint + FileTypes FileTypes + ImageFileConstraint } FileTypes struct { AcceptAny bool @@ -20,17 +20,19 @@ type ( ) const ( - FileQuestionFileTypeField = "fileTypes" - FileConstraintsCustomsField = "fileConstraint" + FileQuestionFileTypeField = "fileTypes" + FileImageConstraintField = "img_c" ) func NewFileQuestion( - id id.QuestionID, text string, fileTypes FileTypes, constraint map[FileType]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), - FileTypes: fileTypes, - Constraints: constraint, + Basic: NewBasic(id, text, TypeFile, formID), + FileTypes: fileTypes, + ImageFileConstraint: imgConstraint, } } @@ -61,40 +63,23 @@ func ImportFileQuestion(q StandardQuestion) (*FileQuestion, error) { AcceptPDF: fileTypeData[2], } - constraintsCustomsData, has := q.Customs[FileConstraintsCustomsField] + imgConstraintCustomR, has := q.Customs[FileImageConstraintField] //if FileConstraintsCustomsField is not present, return FileQuestion without constraint if !has { - return NewFileQuestion(q.ID, q.Text, fileTypes, nil, q.FormID), nil + return NewFileQuestion(q.ID, q.Text, fileTypes, ImageFileConstraint{}, q.FormID), nil } - constraintsCustoms, ok := constraintsCustomsData.([]interface{}) + 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)) } - - constraints := make(map[FileType]FileConstraint, len(constraintsCustoms)) - - for _, constraintCustomsDataI := range constraintsCustoms { - constraintCustomsData, ok := constraintCustomsDataI.(map[string]interface{}) - if !ok { - return nil, errors.New( - fmt.Sprintf("\"%s\" must be map[string]interface{} for FileQuestion", FileConstraintsCustomsField)) - } - - st, err := NewStandardFileConstraint(FileType(constraintCustomsData[FileTypeCustomField].(float64)), constraintCustomsData) - if err != nil { - return nil, fmt.Errorf("failed to import file constraint: %w", err) - } - fileType, constraint, err := ImportFileConstraint(*st) - if err != nil { - return nil, fmt.Errorf("failed to import file constraint: %w", err) - } - constraints[fileType] = constraint + imgConstraint, err := ImportImageFileConstraint(imgConstraintCustom) + if err != nil { + return nil, fmt.Errorf("failed to import ImageFileConstraint: %w", err) } - - question := NewFileQuestion(q.ID, q.Text, fileTypes, constraints, q.FormID) + question := NewFileQuestion(q.ID, q.Text, fileTypes, *imgConstraint, q.FormID) return question, nil } @@ -103,16 +88,5 @@ func (q FileQuestion) Export() (*StandardQuestion, error) { qt := []bool{q.FileTypes.AcceptAny, q.FileTypes.AcceptImage, q.FileTypes.AcceptPDF} customs[FileQuestionFileTypeField] = qt - - if q.Constraints != nil { - constraints := make([]map[string]interface{}, 0, len(q.Constraints)) - for _, constraint := range q.Constraints { - c, err := constraint.Export() - if err != nil { - return nil, fmt.Errorf("failed to export file constraint: %w", err) - } - constraints = append(constraints, c.Customs) - } - } return NewStandardQuestion(TypeFile, q.ID, q.FormID, q.Text, customs), nil } 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 From 311393f397c5cde611ea6d24a52683e0669dc79e Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Sun, 26 May 2024 23:36:54 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(model/question)=20m?= =?UTF-8?q?ove=20ImageSpecs=20to=20new=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/question/file_constraint_image.go | 111 --------------- .../question/file_constraint_image_spec.go | 130 ++++++++++++++++++ 2 files changed, 130 insertions(+), 111 deletions(-) create mode 100644 svc/pkg/domain/model/question/file_constraint_image_spec.go diff --git a/svc/pkg/domain/model/question/file_constraint_image.go b/svc/pkg/domain/model/question/file_constraint_image.go index b734238..c9b7c60 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -9,17 +9,6 @@ import ( ) type ( - DimensionSpec struct { - Min, Max *int - Eq *int - } - - // RatioSpec represents the ratio of width / height - RatioSpec struct { - Min, Max *float32 - Eq *float32 - } - ImageFileConstraint struct { Ratio RatioSpec MinNumber *int @@ -272,76 +261,6 @@ func ImportImageFileConstraint(c map[string]interface{}) (*ImageFileConstraint, return NewImageFileConstraint(minNumber, maxNumber, width, height, ratio) } -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 -} - func loadInt(t map[string]interface{}, key string) (*int, error) { v, has := t[key] if !has { @@ -368,33 +287,3 @@ func (c ImageFileConstraint) Export() map[string]interface{} { result[FileImageConstraintHeight] = heightC return result } - -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 -} 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 +} From d849a8bd131172306d803b6f025eeadb7813819f Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 27 May 2024 00:06:07 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E2=9C=85=20(model/question)=20TestImport?= =?UTF-8?q?FileQuestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file_test.go | 126 +++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 svc/pkg/domain/model/question/file_test.go 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..3162220 --- /dev/null +++ b/svc/pkg/domain/model/question/file_test.go @@ -0,0 +1,126 @@ +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 + 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}, + }, + }, + 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), + }, + } + 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) + }) + } +} From 84629bef4418349a3f0e18e6212856ecd0f96693 Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 27 May 2024 00:25:37 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=90=9B=20(infra/reader)=20Export?= =?UTF-8?q?=E3=81=AE=E3=83=A1=E3=82=BE=E3=83=83=E3=83=88=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/infra/reader/question_test.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 From 2dbe3f65c88815bbaefee391a372f2d82e292dba Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 27 May 2024 01:16:08 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=90=9B=20(model/question)=20fix=20p?= =?UTF-8?q?arsing,=20add=20export->import=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file.go | 1 + svc/pkg/domain/model/question/file_constraint_image.go | 2 ++ svc/pkg/domain/model/question/file_test.go | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/svc/pkg/domain/model/question/file.go b/svc/pkg/domain/model/question/file.go index 5ddee16..7a05698 100644 --- a/svc/pkg/domain/model/question/file.go +++ b/svc/pkg/domain/model/question/file.go @@ -88,5 +88,6 @@ func (q FileQuestion) Export() (*StandardQuestion, error) { 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_image.go b/svc/pkg/domain/model/question/file_constraint_image.go index c9b7c60..7286747 100644 --- a/svc/pkg/domain/model/question/file_constraint_image.go +++ b/svc/pkg/domain/model/question/file_constraint_image.go @@ -285,5 +285,7 @@ func (c ImageFileConstraint) Export() map[string]interface{} { 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_test.go b/svc/pkg/domain/model/question/file_test.go index 3162220..a710e26 100644 --- a/svc/pkg/domain/model/question/file_test.go +++ b/svc/pkg/domain/model/question/file_test.go @@ -24,6 +24,11 @@ func TestImportFileQuestion(t *testing.T) { 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, @@ -121,6 +126,10 @@ func TestImportFileQuestion(t *testing.T) { 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) + } }) } } From 5259aa05c8bce27c8b4af6ca64ee55f51ef2434d Mon Sep 17 00:00:00 2001 From: Shion Ichikawa Date: Mon, 27 May 2024 02:55:28 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9C=85=20(model/question)=20ImageConst?= =?UTF-8?q?raint=20MinMaxNumber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svc/pkg/domain/model/question/file_test.go | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/svc/pkg/domain/model/question/file_test.go b/svc/pkg/domain/model/question/file_test.go index a710e26..cbb9d2c 100644 --- a/svc/pkg/domain/model/question/file_test.go +++ b/svc/pkg/domain/model/question/file_test.go @@ -9,6 +9,7 @@ import ( 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 @@ -114,6 +115,39 @@ func TestImportFileQuestion(t *testing.T) { 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) {