Form Service
Form Service (Port 8010)
The form-service provides the engine for web-based data collection. It renders surveys created in the survey-service as interactive web forms, evaluates skip logic and constraints in real time, and manages the submission pipeline.
Database
Database: mquad_forms_db
| Table | Purpose |
|---|---|
form_sessions | Active respondent sessions for resuming drafts |
form_submissions | Temporary staging for incoming submissions |
API Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/forms/{survey_id}/render | Get rendered form structure (JSON) |
| POST | /api/v1/forms/submit | Submit finalized form data |
| POST | /api/v1/forms/draft | Save partial progress |
| GET | /api/v1/forms/resume/{token} | Resume a saved draft |
| POST | /api/v1/forms/validate | Validate form data without submitting |
| POST | /api/v1/validation/field | Validate a single field constraint |
| POST | /api/v1/validation/evaluate | Evaluate an XPath expression with form data |
| GET | /api/v1/validation/functions | List all available XPath functions |
| POST | /api/v1/validation/test-constraint | Quick test of a constraint expression |
Expression Engine
The form-service includes a complete XPath 1.0 expression evaluator with ODK/XLSForm extensions. The same engine powers skip logic (relevance), constraints, and calculations.
Architecture
The evaluator has four layers:
- Tokenizer (
XPathTokenizer) — Converts expression strings into tokens: numbers, strings, operators, function names, field references (${name}), parentheses. - Parser (
XPathParser) — Recursive-descent parser that builds an AST (Abstract Syntax Tree) from tokens. Supports operator precedence:or(lowest) →and→ comparison (=,!=,<,>,<=,>=) → arithmetic (+,-) → multiplicative (*,div,mod) → unary (-,not). - Evaluator (
XPathEvaluator) — Walks the AST against form data. Resolves${field}references to answer values, dispatches function calls to the function registry. - Function Registry (
ODKFunctionRegistry) — 40+ registered ODK/XLSForm functions, all case-insensitive.
Supported Functions
String Functions
| Function | Description |
|---|---|
string-length(value) | Length of string |
substring(value, start, [length]) | Substring (1-based start) |
contains(value, substr) | Check if string contains |
starts-with(value, prefix) | Check string prefix |
normalize-space(value) | Trim and collapse whitespace |
concat(a, b, ...) | Concatenate strings |
translate(value, from, to) | Character translation |
regex(value, pattern) | Regex match |
Number Functions
| Function | Description |
|---|---|
number(value) | Convert to number |
int(value) | Convert to integer |
round(value, [precision]) | Round to precision |
floor(value) | Floor |
ceiling(value) | Ceiling |
abs(value) | Absolute value |
Date/Time Functions
| Function | Description |
|---|---|
today() | Current date |
now() | Current datetime |
date(value) | Parse string to date |
format-date(value, format) | Format date using strftime |
decimal-date-time(value) | Days since epoch |
days-between(a, b) | Days between two dates |
today-offset(days) | Date offset from today |
ODK-Specific Functions
| Function | Description |
|---|---|
selected(value, choice) | True if choice selected (multi-select) |
count-selected(value) | Count selected options |
selected-at(value, index) | Choice at position |
count(a, b, ...) | Count non-empty args |
sum(a, b, ...) | Sum numeric args |
min(a, b, ...) | Minimum value |
max(a, b, ...) | Maximum value |
join(sep, a, b, ...) | Join with separator |
coalesce(a, b, ...) | First non-empty value |
Conditional Functions
| Function | Description |
|---|---|
if(cond, then, else) | Conditional evaluation |
boolean(value) | Convert to boolean |
Utility Functions
| Function | Description |
|---|---|
random() | Random 0-1 |
uuid() | Generate UUID |
position() | Current repeat position |
last() | Last repeat position |
empty(value) | Check if empty |
Constraint Validation
Constraints are evaluated at two points:
Client-side (during form fill): The ConstraintValidator evaluates each constraint expression against current form data using the XPathEvaluator. Empty values skip constraint evaluation (non-required fields pass validation when unanswered).
Server-side (on submit): FormSubmissionService._validate_form_data() runs all constraints server-side before persisting. If validation fails and the submission is marked complete, the submission is rejected with validation errors.
Date Arithmetic
Date expressions use days-between() and today-offset() for reliable cross-platform date arithmetic:
days-between(today(), ${dob}) >= 6570 -- Age >= 18 years
. <= today-offset(-10) -- Within last 10 days
. >= today-offset(10) -- Within next 10 daysDirect date subtraction (today() - ${dob}) is converted to days-since-epoch automatically. The _to_numeric() method in the evaluator handles date objects by converting to days since January 1, 1970 before performing arithmetic.
Multi-Select Functions
Multi-select questions store selections as space-separated strings in ODK format. The selected(), count-selected(), and selected-at() functions parse this format:
selected(${colors}, 'red') -- Is 'red' selected?
count-selected(${colors}) > 1 -- More than one selected?
selected-at(${colors}, 0) = 'red' -- First selection is 'red'?Cross-Platform Parity
The same expression engine runs on:
- Web — form-service (Python FastAPI)
- Mobile — Flutter field app (Dart)
Both implement identical tokenizer, parser, function registry, and evaluation logic. Test suites verify deterministic output across both platforms for all 40+ functions.
Form Runtime Logic
The service implements XLSForm-compatible logic evaluation:
- Skip Logic (Relevance) — Dynamically hides/shows questions based on expressions. The survey designer's visual builder generates XPath relevance expressions. During form fill, the engine evaluates relevance on every answer change and updates visible questions.
- Constraints — Real-time validation of answers against range, regex, or custom expressions. Constraints run on every answer change. Violations show the constraint message immediately.
- Calculations — Auto-computation of hidden fields using expression evaluation.
- Repeat Groups — Management of nested, multi-instance question blocks.
Choice Constraints (Per-Choice Visibility)
Each choice in a select list can have a constraint expression defined in the choices sheet of the XLSForm. These per-choice constraints control whether individual options are visible to the respondent.
Evaluation flow at render time:
- The full choice list (including
constraintfields) is included in the rendered form schema. - The frontend (web) and mobile app evaluate per-choice constraints during form fill.
- Both platforms use the same expression evaluator for deterministic behavior.
Constraint evaluation order:
- Question-level
choice_filter— Filters choices based on extra column values matching question answers. - Per-choice constraint — Each remaining choice's constraint expression is evaluated against form data. Choices whose constraint returns
falseare removed. - Lookup data filter — For referee questions with lookup assignments, choices are further restricted.
Expression context during evaluation:
- The dot
.resolves to the choice'sname. - The
labelvariable resolves to the choice'slabel. - All other question answers are available by name.
- The choice's extra columns (from the choices sheet) are also available as variables.
Responsive Design
The form rendering engine is optimized for both mobile and desktop browsers:
- Paging — Support for single-page and multi-page form layouts.
- Media Support — Direct integration with the
media-servicefor file uploads. - Offline Resilience — Local caching of form structures to prevent data loss.
Submission Workflow
- Respondent fills out the form. Skip logic updates dynamically on every answer.
- Constraint validation runs on every answer change — violations are shown immediately.
- On submit, the service validates all constraints server-side.
- If valid, data is pushed to the
data-servicefor permanent storage. form.submittedevent is published to Redis.
