relevant  ·  constraint  ·  required  ·  calculate

Um formulário é um
programa pequeno.

Enquanto nada o executa, um servidor só pode aceitar o que o aparelho mandou e torcer para que o aparelho estivesse certo. O rxeval é esse algo: XPath 1.0 com as extensões do OpenRosa, avaliado sobre a instância de um formulário, sem dependências — no servidor, no navegador via WebAssembly, no Python e no R.

uma entrevista, no meio de uma resposta
session.set("/data/age", "9")?;
let outcome = session.recompute();

// o que o formulário diz agora, e só o que mudou
outcome.calculated   {"/data/adult": "no", "/data/years_left": "9"}
outcome.relevant     {"/data/guardian": true}
outcome.missing      ["/data/guardian"]
outcome.invalid      []

Um responsável só é exigido para criança, então ele passa a faltar no instante em que a idade diz nove — e deixa de faltar no instante em que diz quarenta.

o problema que este crate existe para resolver

Não existe uma língua ODK única.

Formulários ODK são avaliados por duas implementações: o JavaRosa, dentro do ODK Collect e do KoboCollect, e o openrosa-xpath-evaluator do Enketo, dentro dos formulários web. Nenhum contém o outro, e as diferenças não se anunciam: uma expressão que o Collect não sabe avaliar costuma devolver nada em vez de erro. O formulário preenche, a entrevista termina, e uma coluna volta vazia.

expressão JavaRosa Enketo o rxeval segue
resident[2]/namenadao segundoEnketo
last() · floor() · ceiling()ausentepresenteEnketo
//namesem eixo //busca em descendentesEnketo
regex("a123…b", "[0-9]{11}")ancorado → falsosem âncora → verdadeiroJavaRosa
round(-1.5)−1 (meio para cima)−2 (para longe do zero)Enketo
boolean-from-string("TRUE")verdadeirofalsoJavaRosa
distance() · area()precisão cheiaarredondado a 2 casasJavaRosa

Sete de vinte e seis. Cada regra deste crate foi decidida pondo a mesma expressão diante dos dois motores e lendo as duas respostas — e cada divergência está escrita na suíte de testes, com o lado que o rxeval segue e por quê. O teste falha se uma divergência registrada parar de divergir, porque isso também é notícia.

Um corpus de 126 expressões é submetido ao JavaRosa, ao avaliador do Enketo e ao rxeval; as duas referências concordam em 99 e divergem em 26.

antes de alguém coletar com ele

Quais das suas regras não vão viajar.

Como as duas línguas diferem, um formulário pode estar certo num lugar e silenciosamente errado no outro. O rxeval lê o formulário e avisa — com o bind a que pertence, o que quebra onde, e o que escrever no lugar.

rxeval::check_form(&xform)?
/data/resident[2]/name (relevant): a bare positional predicate —
  JavaRosa returns nothing for [n]; use [position() = n] on Collect /
  KoboCollect. Write [position() = 2] instead.

/data/total (calculate): /p/morador/maior, which this form's instance
  has no node for — the path matches nothing, so the rule reads an empty
  node-set: a calculation comes out 0, a comparison comes out false, and a
  relevant hides its question for the whole of fieldwork, identically on
  both engines. Write /data/morador/maior instead.

A segunda nem é problema de portabilidade: ela viaja perfeitamente e está errada em todo lugar aonde vai. Aparece aqui porque é encontrada do mesmo jeito e importa mais — nada mais no ecossistema pega isso, e um cálculo que sai zero em silêncio é descoberto quando o relatório é escrito, não quando o dado é coletado.

duas direções

Julgar uma submissão, e preencher uma.

O Rules responde sobre uma submissão pronta: isto é válido, quanto dá o cálculo, quais nós eram relevantes. É a forma certa para um servidor conferindo o que chegou, e a forma errada para uma tela em que alguém está digitando.

A Session é a outra direção. Ela guarda a instância, aplica o que o formulário deriva e reporta o que se moveu. Os cálculos rodam na ordem de dependência, então um que alimenta o outro se acomoda num passo só; e só voltam os caminhos cujo valor de fato mudou, porque um renderizador que redesenha todo campo calculado a cada tecla briga com o cursor.

É de propósito o mesmo motor da checagem no servidor. Um formulário que se comporta de um jeito sendo preenchido e de outro ao chegar é pior que um errado de forma consistente: só o primeiro produz dado que ninguém sabe explicar.

uma repetição, linha a linha
session.add_row("/data/resident")?;
session.set("/data/resident[1]/age", "40")?;
session.set("/data/resident[2]/age", "9")?;
session.recompute();

session.get("/data/resident[1]/adult")   "1"
session.get("/data/resident[2]/adult")   "0"
session.get("/data/total_adults")        "1"

// e a relevância também é por linha
outcome.relevant["/data/resident[2]/works"]  falso

uma implementação, compilada de quatro jeitos

A mesma resposta no servidor, no celular e no script que escreveu o formulário.

Um formulário web que perguntasse ao servidor o que as próprias regras significam precisaria de conexão a cada tecla, o que exclui justamente o lugar onde a pesquisa acontece — um ponto de ônibus, uma porta de casa, um subsolo. Compilar o mesmo Rust para WebAssembly tira a rede da entrevista sem criar uma segunda implementação para divergir da primeira.

Um XForm e uma instância entram no rxeval, que compila para código nativo, WebAssembly, Python e R; os quatro dão o mesmo veredito.

native

Um servidor conferindo o que chegou, contra o formulário que publicou.

WebAssembly

Um navegador preenchendo um formulário sem sinal, decidindo localmente.

Python

O script que escreve o formulário, conferindo antes de publicá-lo. O pyxform é Python.

R

Um questionário lido a partir de um script de análise, antes do campo.

instalar

Uma dependência, e ela é opcional.

Cargo.toml
[dependencies]
rxeval = "0.1"
menor, para WebAssembly
rxeval = { version = "0.1",
           default-features = false }

A única dependência é um motor de regex, atrás de uma feature. Uma build sem ele recusa o regex() em vez de adivinhar: uma regra que não rodou não é uma regra que passou.