ตรวจความถูกต้องของคำตอบฟอร์ม (Validation Engine)
ภาพรวม
เครื่องยนต์ validation ที่รันฝั่ง server ก่อนบันทึกคำตอบ ถือเป็นด่านจริงไม่ใช่เพียง UI hint เพราะแม้ฝั่ง client-web จะ validate มาก่อนแล้ว แต่ request ที่ทำมือสามารถข้ามด่านนั้นไปได้ ตัว engine เป็น service ที่ stateless ล้วนและไม่แตะ database
จุดที่ต้องเข้าใจคือ engine นี้ port มาจาก JS จึงต้องเลียนพฤติกรรมเฉพาะตัวของ JS ให้ตรง เช่นการนับความยาวต้องนับเป็น UTF-16 code unit เหมือน String.length ไม่ใช่นับเป็น byte เพื่อให้ข้อความภาษาไทยผ่านหรือไม่ผ่านเหมือนเดิมทุกกรณี
Business Flow
ValidateAnswers(questions, answers)
- วนเฉพาะ คำตอบที่ส่งมา เท่านั้น ทำให้ field ที่ required แต่ไม่ได้ส่งมาเลยจะไม่ถูกตรวจ ซึ่งเป็น parity กับ source
- key ที่ไม่ตรงกับคำถามใดจะให้ error
Question with ID {id} not found in form definition - คำถามที่ไม่ required และมีค่าว่าง (
""หรือnull) จะถูกข้าม - ที่เหลือถูกส่งเข้า
validateSingleAnswerโดยข้อความ error จะถูก prefix ด้วยชื่อคำถาม ซึ่งไล่ลำดับจากtitleไปplaceholderและสุดท้ายเป็นQuestion {id} - คืนผลลัพธ์เป็น
{IsValid, Errors}แล้ว caller จะแปลงเป็น 400 body แบบ object
validateSingleAnswer(q, value)
- coerce ค่าให้เป็น string โดย string จะถูก trim, number ใช้รูปแบบของ JS, boolean กลายเป็น
"true"หรือ"false"และชนิดอื่น ๆ ถูกแปลงเป็น JSON - ค่าว่างที่ไม่ required จะผ่าน ส่วน required ที่ว่างจะได้
This field is required commonRule.patternมีความสำคัญเหนือกฎตามชนิด field หากมีจะใช้ pattern นั้นพร้อมตรวจminLengthและmaxLengthของ commonRule ทันที และเลือกข้อความ error ตามลำดับerrorMessageTh→errorMessage→ ข้อความ default- หากไม่มี commonRule pattern จึงเข้าสู่กฎตามชนิด field
emailใช้ default^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$และ override ด้วยq.ruleได้phoneใช้ default^[0-9]{10,15}$และหากมีq.ruleจะตัดเครื่องหมาย"ออกก่อนnumberใช้^[0-9]+$,urlใช้^(https?|ftp)://[^\s/$.?#].[^\s]*$และcitizen_idใช้^[0-9]{13}$customใช้q.regexแบบดิบ ส่วนcustom_inputจะตัด/หน้าและท้ายออกก่อน พร้อมรองรับ min/maxLength และข้อความภาษาไทยจาก commonRule โดย default คือกรุณากรอกข้อมูลให้ถูกต้องdateใช้^\d{4}-\d{2}-\d{2}$date_of_birthตรวจรูปแบบวันที่แล้วคำนวณอายุจากปีปัจจุบันลบปีเกิด และลบอีก 1 หากยังไม่ถึงวันเกิด หากต่ำกว่าminAgeจะได้Age must be at least N yearsshort_textจำกัด 1 ถึง 255 ตัวอักษร,paragraphจำกัด 1 ถึง 5000 และcustom_textใช้minLength/maxLengthของคำถามเอง โดย default คือ 1 ถึง 1000termsต้องมีค่าเป็นtrueหรือ"true"มิฉะนั้นจะได้You must agree to the terms and conditionsfile_uploadผ่านเสมอ- ชนิดตัวเลือกได้แก่
single_choice,multiple_choice,dropdownและmulti_select_dropdownยังปิดการตรวจไว้ ซึ่งตรงกับ source ที่ comment โค้ดส่วนนี้ไว้เช่นกัน - ชนิดที่ไม่รู้จักจะผ่าน
- หากมี regex และค่าไม่ว่าง ระบบจะ compile แล้ว match โดย pattern จะถูกแปลงจาก JS escape (
\uXXXX,\u{..}) ให้เป็นรูปแบบ RE2 (\x{..}) ก่อนด้วยutil.TranslateJSRegex - pattern ที่ RE2 compile ไม่ได้ เช่น lookaround หรือ backreference ซึ่ง Go ไม่รองรับ จะไม่ถือเป็น error ของผู้ใช้ แต่คืนข้อความ
Invalid validation pattern configured for this fieldเพื่อสื่อว่าเป็น config error
ไฟล์และฟังก์ชันหลัก
feature นี้ไม่มี route ของตัวเอง แต่ถูกเรียกจาก formsubmission.Service.Submit
| ไฟล์ | ฟังก์ชัน |
|---|---|
internal/formsubmission/validation.go | NewFormValidationService(), (*FormValidationService).ValidateAnswers, validateSingleAnswer, validateTextLength, computeAge, utf16Len, stripSlashes, firstNonEmpty, formatJSNumber, decodeQuestionMap, isEmptyValue |
internal/util/jsregex.go | TranslateJSRegex |
internal/formsubmission/errors.go | newValidationError(errors) ซึ่งสร้าง 400 body แบบ object |
internal/formsubmission/entity.go | ค่าคงที่ชนิดคำถามทั้งหมด เช่น QFirstName, QEmail, QPhone, QCustomInput, QDateOfBirth, QTerms |
จุดเชื่อมต่อกับ Service อื่น
- อ่านนิยามคำถามจาก
form_builder.questionsที่ผ่านtransformPatternมาแล้ว จึงมี keycommonRuleติดมาด้วย ดูรายละเอียดที่ โหลดโครงฟอร์ม และ กฎ Validation กลาง - ถูกเรียกใน ส่งคำตอบฟอร์ม ที่ขั้นตอนที่ 5 คือก่อน profile mapping และก่อนด่าน OTP
- ไม่แตะตารางหรือ service ภายนอกใด ๆ
- ฝั่ง client-web ที่เกี่ยวข้องคือ feature
form-fillเนื่องจากข้อความ error ที่แสดงบนฟอร์มมาจาก engine นี้