Merge branch 'web-server'

This commit is contained in:
Álvaro González 2026-02-01 16:17:34 +01:00
commit 6fe64c1d85
10 changed files with 109 additions and 18 deletions

View file

@ -10,6 +10,16 @@ ENV PUB_CACHE=/cache/pub-cache
ENV GRADLE_USER_HOME=/cache/gradle-cache ENV GRADLE_USER_HOME=/cache/gradle-cache
RUN mkdir -p $PUB_CACHE $GRADLE_USER_HOME RUN mkdir -p $PUB_CACHE $GRADLE_USER_HOME
# To avoid: fatal: detected dubious ownership in repository at '/sdks/flutter'
# Pass this during build: --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g)
ARG USER_ID
ARG GROUP_ID
RUN echo "Using UID: $USER_ID and GID: $GROUP_ID"
RUN chown -R $USER_ID:$GROUP_ID $PUB_CACHE $GRADLE_USER_HOME
RUN chown -R $USER_ID:$GROUP_ID /sdks/flutter
USER $USER_ID:$GROUP_ID
# Copy pubspec files and get dependencies # Copy pubspec files and get dependencies
# Commented out to avoid building the app during image creation, this will be handled externally by makefile # Commented out to avoid building the app during image creation, this will be handled externally by makefile
# COPY pubspec.yaml pubspec.lock ./ # COPY pubspec.yaml pubspec.lock ./

View file

@ -19,11 +19,14 @@ build-linux-debug-container: pub-get-container
build-web-debug-container: pub-get-container build-web-debug-container: pub-get-container
./docker-exec.sh exec flutter build web --debug ./docker-exec.sh exec flutter build web --debug
run-linux-debug-container: build-linux-debug-container run-linux-debug-container: pub-get-container
./docker-exec.sh exec flutter run -d linux ./docker-exec.sh exec flutter run -d linux
run-linux-debug: build-linux-debug-container run-web-debug-container: pub-get-container
./docker-exec.sh exec flutter run --web-port $${WEB_PORT:-8081} -d web-server
run-linux-debug-native: build-linux-debug-container
./build/linux/x64/debug/bundle/d4rt_formulas ./build/linux/x64/debug/bundle/d4rt_formulas
run-web-debug-container: build-web-debug-container run-web-debug-native: build-web-debug-container
cd build/web && python3 -m http.server 8080 cd build/web && python3 -m http.server $${WEB_PORT:-8081}

View file

@ -132,9 +132,9 @@ Where:
{"name": "Reflexes", "values": ["No response", "Grimace on aggressive stimulation", "Cry on stimulation"] }, {"name": "Reflexes", "values": ["No response", "Grimace on aggressive stimulation", "Cry on stimulation"] },
{"name": "SkinColor", "values": ["Blue or pale", "Blue extremities, pink body", "Pink"] } {"name": "SkinColor", "values": ["Blue or pale", "Blue extremities, pink body", "Pink"] }
], ],
"output": {"name": "Result", "unit": "scalar"}, "output": {"name": "Result", "unit": "string"},
"d4rtCode": """ "d4rtCode": """
var total = HeartRate + Breathing + MuscleTone + Reflexes + SkinColor; var total = indexOf("HeartRate") + indexOf("Breathing") + indexOf("MuscleTone") + indexOf("Reflexes") + indexOf("SkinColor");
late var interpretation; late var interpretation;
if( total < 4 ) { if( total < 4 ) {
interpretation = 'Critical condition'; interpretation = 'Critical condition';
@ -154,12 +154,12 @@ Where:
"name": "Compare price per mass", "name": "Compare price per mass",
"description": "Compares two products by their price per mass and returns which is cheaper.", "description": "Compares two products by their price per mass and returns which is cheaper.",
"input": [ "input": [
{"name": "price1", "unit": "scalar"}, {"name": "price1", "unit": "currency"},
{"name": "mass1", "unit": "kilogram"}, {"name": "mass1", "unit": "kilogram"},
{"name": "price2", "unit": "scalar"}, {"name": "price2", "unit": "currency"},
{"name": "mass2", "unit": "kilogram"} {"name": "mass2", "unit": "kilogram"}
], ],
"output": {"name": "Result", "unit": "scalar"}, "output": {"name": "Result", "unit": "string"},
"d4rtCode": """ "d4rtCode": """
var p1 = price1 / mass1; var p1 = price1 / mass1;
var p2 = price2 / mass2; var p2 = price2 / mass2;

View file

@ -0,0 +1,3 @@
[
{"name": "currency", "symbol": "¤", "isBase": true},
]

View file

@ -1,3 +1,4 @@
[ [
{"name": "scalar", "symbol": "", "isBase": true}, {"name": "scalar", "symbol": "㊷", "isBase": true},
{"name": "string", "symbol": "🔤", "isBase": true}
] ]

View file

@ -19,9 +19,12 @@ detect_container(){
fi fi
} }
clean_build_cache(){
$DOCKER builder prune --all --force
}
build_image(){ build_image(){
$DOCKER build -t d4rt-formulas-builder -f Dockerfile . $DOCKER build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) --progress=plain -t d4rt-formulas-builder -f Dockerfile .
} }
graphic_options(){ graphic_options(){
@ -45,19 +48,37 @@ graphic_options(){
fi fi
} }
exec_in_container(){ spi_options(){
local SPIOPTIONS="--env AT_SPI_BUS=/run/user/$(id -u)/at-spi/bus_0 --volume=/run/user/$(id -u)/at-spi:/run/user/$(id -u)/at-spi --device=/dev/dri" if [ -e /run/user/$(id -u)/at-spi/bus_0 ]
then
printf " %s " "--env AT_SPI_BUS=/run/user/$(id -u)/at-spi/bus_0"
fi
if [ -e /run/user/$(id -u)/at-spi ]
then
printf " %s " "--volume=/run/user/$(id -u)/at-spi:/run/user/$(id -u)/at-spi"
fi
if [ -e /dev/dri ]
then
printf " %s " "--device /dev/dri"
fi
}
exec_in_container(){
SPIOPTIONS=$(spi_options)
local GRAPHICOPTIONS=$(graphic_options) local GRAPHICOPTIONS=$(graphic_options)
local BUILDCACHE=./.build-container-cache local BUILDCACHE=./.build-container-cache
mkdir -p $BUILDCACHE mkdir -p $BUILDCACHE
$DOCKER run \ $DOCKER run \
-it \ -it \
--userns=keep-id \
--user $(id -u):$(id -g) \
--init \ --init \
--rm \ --rm \
$GRAPHICOPTIONS \ $GRAPHICOPTIONS \
$SPIOPTIONS \ $SPIOPTIONS \
-p ${WEBPORT:-8081}:8081 \
-v $BUILDCACHE:/cache:z \ -v $BUILDCACHE:/cache:z \
-v .:/app:z \ -v .:/app:z \
-e FLUTTER_FLAVOR=prod \ -e FLUTTER_FLAVOR=prod \
@ -73,12 +94,18 @@ main(){
return $? return $?
fi fi
if [ "$1" = "cleancache" ]; then
clean_build_cache
return $?
fi
if [ "$1" = "exec" ]; then if [ "$1" = "exec" ]; then
exec_in_container ${@:2} exec_in_container ${@:2}
return $? return $?
fi fi
echo "Usage: $0 {build|exec <command>}" echo "Usage: $0 {build|cleancache|exec <command>}"
return 1 return 1
} }

View file

@ -94,7 +94,6 @@ class _FormulaScreenState extends State<FormulaScreen> {
void _evaluateFormula() { void _evaluateFormula() {
print( "EVALUATE FORMULA"); print( "EVALUATE FORMULA");
if (!_formKey.currentState!.validate()) return;
try { try {
final inputValues = <String, dynamic>{}; final inputValues = <String, dynamic>{};
@ -145,7 +144,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
// Convert output to selected unit if needed // Convert output to selected unit if needed
String? unit = widget.formula.output.unit; String? unit = widget.formula.output.unit;
if (unit != null && unit is Number) { if (unit != null && result is Number) {
final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!); final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!);
if (converted is num) { if (converted is num) {
_result = converted.toStringAsFixed(2); _result = converted.toStringAsFixed(2);
@ -274,6 +273,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
onUnitChanged: (unit) { onUnitChanged: (unit) {
_selectedOutputUnit = unit; _selectedOutputUnit = unit;
_evaluateFormula(); _evaluateFormula();
print( "En output unit changed to $unit: $_result");
setState(() { setState(() {
}); });
}, },

View file

@ -20,6 +20,7 @@ Future<Corpus> createDefaultCorpus() async{
final unitResources = [ final unitResources = [
"assets/units/angle.d4rt.units", "assets/units/angle.d4rt.units",
"assets/units/area.d4rt.units", "assets/units/area.d4rt.units",
"assets/units/currency.d4rt.units",
"assets/units/distance.d4rt.units", "assets/units/distance.d4rt.units",
"assets/units/energy.d4rt.units", "assets/units/energy.d4rt.units",
"assets/units/force.d4rt.units", "assets/units/force.d4rt.units",
@ -32,6 +33,7 @@ Future<Corpus> createDefaultCorpus() async{
]; ];
for (final unitRes in unitResources) { for (final unitRes in unitResources) {
print( "Loading units from $unitRes");
final literal = await loadResourceAsString(unitRes); final literal = await loadResourceAsString(unitRes);
final units = UnitSpec.fromArrayStringLiteral(literal); final units = UnitSpec.fromArrayStringLiteral(literal);
corpus.loadUnits(units); corpus.loadUnits(units);

View file

@ -104,8 +104,9 @@ class FormulaEvaluator {
final result = _interpreter.execute(source: completeSource); final result = _interpreter.execute(source: completeSource);
return result; return result;
} }
catch (e) { catch (e, stack) {
print( "Error evaluating formula source:\n$completeSource" ); print( "Error evaluating formula source:\n$completeSource" );
print( stack );
throw FormulaEvaluationException( throw FormulaEvaluationException(
'Error evaluating formula "${formula.name}": $e', 'Error evaluating formula "${formula.name}": $e',
e, e,
@ -141,6 +142,8 @@ class FormulaEvaluator {
import "package:d4rt_formulas.dart"; import "package:d4rt_formulas.dart";
"""; """;
static const reservedVariableNames = { "variableValues", "indexOf", "variableAllowedValues"} ;
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) { String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) {
final buffer = StringBuffer(); final buffer = StringBuffer();
@ -168,6 +171,29 @@ class FormulaEvaluator {
"""); """);
} }
} }
buffer.writeln("""
final variableValues = <String, dynamic>{
""");
for (final entry in inputValues.entries) {
final varName = entry.key;
final value = entry.value;
if (value is String) {
final escapedValue = value.replaceAll('"', '\\"');
buffer.writeln("""
"$varName": "$escapedValue",
""");
} else {
buffer.writeln("""
"$varName": "$value",
""");
}
}
buffer.writeln("""
};
""");
// Build a Map<String, List<String>> named `variableValues` that exposes allowed values // Build a Map<String, List<String>> named `variableValues` that exposes allowed values
// for each VariableSpec (inputs and output) to the interpreted code. Values are // for each VariableSpec (inputs and output) to the interpreted code. Values are
// converted to strings and quoted in the produced d4rt source. // converted to strings and quoted in the produced d4rt source.
@ -187,13 +213,24 @@ class FormulaEvaluator {
} }
// Write the variableValues map into the generated source without escaping names/values // Write the variableValues map into the generated source without escaping names/values
buffer.writeln("final variableValues = {"); buffer.writeln("final variableAllowedValues = {");
variableValuesMap.forEach((name, list) { variableValuesMap.forEach((name, list) {
final listLiteral = list.map((s) => '"' + s + '"').join(', '); final listLiteral = list.map((s) => '"' + s + '"').join(', ');
buffer.writeln(' "' + name + '": [' + listLiteral + '],'); buffer.writeln(' "' + name + '": [' + listLiteral + '],');
}); });
buffer.writeln('};'); buffer.writeln('};');
// Some functions to deal with string values
buffer.writeln("""
// If return type is int, there is an error converting double to int 🤷
dynamic indexOf(String inputName) {
String value = variableValues[inputName];
String allowedValues = variableAllowedValues[inputName];
dynamic ret = allowedValues.indexOf(value) as int;
return ret as int;
}
""");
buffer.writeln(""" buffer.writeln("""
late var ${formula.output.name}; late var ${formula.output.name};
${formula.d4rtCode} ${formula.d4rtCode}

View file

@ -1,5 +1,6 @@
import 'package:d4rt/d4rt.dart'; import 'package:d4rt/d4rt.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:d4rt_formulas/d4rt_formulas.dart';
abstract class SetUtils { abstract class SetUtils {
static Object safeGet(Map<Object?, Object?> map, String key) { static Object safeGet(Map<Object?, Object?> map, String key) {
@ -105,6 +106,13 @@ class VariableSpec {
final List<dynamic>? values; final List<dynamic>? values;
VariableSpec({required this.name, this.unit, this.values}){ VariableSpec({required this.name, this.unit, this.values}){
validate();
}
void validate(){
if( FormulaEvaluator.reservedVariableNames.contains(name) ){
throw ArgumentError("$name: is a reserved variable name for FormulaEvaluator");
}
final valuesValid = values != null && values?.isNotEmpty == true; final valuesValid = values != null && values?.isNotEmpty == true;
if( unit == null && !valuesValid ){ if( unit == null && !valuesValid ){
throw ArgumentError("$name: at least unit or allowedValues should be valid"); throw ArgumentError("$name: at least unit or allowedValues should be valid");