relevant · constraint · required · calculate
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.
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
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]/name | nada | o segundo | Enketo |
| last() · floor() · ceiling() | ausente | presente | Enketo |
| //name | sem eixo // | busca em descendentes | Enketo |
| regex("a123…b", "[0-9]{11}") | ancorado → falso | sem âncora → verdadeiro | JavaRosa |
| round(-1.5) | −1 (meio para cima) | −2 (para longe do zero) | Enketo |
| boolean-from-string("TRUE") | verdadeiro | falso | JavaRosa |
| distance() · area() | precisão cheia | arredondado a 2 casas | JavaRosa |
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.
antes de alguém coletar com ele
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.
/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
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.
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
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.
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
[dependencies]
rxeval = "0.1"
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.