Form Service

Dynamic web form rendering, expression engine, constraint evaluation, and submission processing

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

TablePurpose
form_sessionsActive respondent sessions for resuming drafts
form_submissionsTemporary staging for incoming submissions

API Endpoints

MethodPathPurpose
GET/api/v1/forms/{survey_id}/renderGet rendered form structure (JSON)
POST/api/v1/forms/submitSubmit finalized form data
POST/api/v1/forms/draftSave partial progress
GET/api/v1/forms/resume/{token}Resume a saved draft
POST/api/v1/forms/validateValidate form data without submitting
POST/api/v1/validation/fieldValidate a single field constraint
POST/api/v1/validation/evaluateEvaluate an XPath expression with form data
GET/api/v1/validation/functionsList all available XPath functions
POST/api/v1/validation/test-constraintQuick 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:

  1. Tokenizer (XPathTokenizer) — Converts expression strings into tokens: numbers, strings, operators, function names, field references (${name}), parentheses.
  2. 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).
  3. Evaluator (XPathEvaluator) — Walks the AST against form data. Resolves ${field} references to answer values, dispatches function calls to the function registry.
  4. Function Registry (ODKFunctionRegistry) — 40+ registered ODK/XLSForm functions, all case-insensitive.

Supported Functions

String Functions

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
if(cond, then, else)Conditional evaluation
boolean(value)Convert to boolean

Utility Functions

FunctionDescription
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 days

Direct 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:

  1. The full choice list (including constraint fields) is included in the rendered form schema.
  2. The frontend (web) and mobile app evaluate per-choice constraints during form fill.
  3. Both platforms use the same expression evaluator for deterministic behavior.

Constraint evaluation order:

  1. Question-level choice_filter — Filters choices based on extra column values matching question answers.
  2. Per-choice constraint — Each remaining choice's constraint expression is evaluated against form data. Choices whose constraint returns false are removed.
  3. Lookup data filter — For referee questions with lookup assignments, choices are further restricted.

Expression context during evaluation:

  • The dot . resolves to the choice's name.
  • The label variable resolves to the choice's label.
  • 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-service for file uploads.
  • Offline Resilience — Local caching of form structures to prevent data loss.

Submission Workflow

  1. Respondent fills out the form. Skip logic updates dynamically on every answer.
  2. Constraint validation runs on every answer change — violations are shown immediately.
  3. On submit, the service validates all constraints server-side.
  4. If valid, data is pushed to the data-service for permanent storage.
  5. form.submitted event is published to Redis.