mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-12 04:41:26 +04:00
js: value destructuring and tests
This commit is contained in:
@@ -6,9 +6,12 @@
|
|||||||
|
|
||||||
#include <storage/storage.h>
|
#include <storage/storage.h>
|
||||||
#include <applications/system/js_app/js_thread.h>
|
#include <applications/system/js_app/js_thread.h>
|
||||||
|
#include <applications/system/js_app/js_value.h>
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define TAG "JsUnitTests"
|
||||||
|
|
||||||
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
|
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -73,7 +76,268 @@ MU_TEST(js_test_storage) {
|
|||||||
js_test_run(JS_SCRIPT_PATH("storage"));
|
js_test_run(JS_SCRIPT_PATH("storage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void js_value_test_compatibility_matrix(struct mjs* mjs) {
|
||||||
|
static const JsValueType types[] = {
|
||||||
|
JsValueTypeAny,
|
||||||
|
JsValueTypeAnyArray,
|
||||||
|
JsValueTypeAnyObject,
|
||||||
|
JsValueTypeFunction,
|
||||||
|
JsValueTypeRawPointer,
|
||||||
|
JsValueTypeInt32,
|
||||||
|
JsValueTypeDouble,
|
||||||
|
JsValueTypeString,
|
||||||
|
JsValueTypeBool,
|
||||||
|
};
|
||||||
|
|
||||||
|
mjs_val_t values[] = {
|
||||||
|
mjs_mk_undefined(),
|
||||||
|
mjs_mk_foreign(mjs, (void*)0xDEADBEEF),
|
||||||
|
mjs_mk_array(mjs),
|
||||||
|
mjs_mk_object(mjs),
|
||||||
|
mjs_mk_number(mjs, 123.456),
|
||||||
|
mjs_mk_string(mjs, "test", ~0, false),
|
||||||
|
mjs_mk_boolean(mjs, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
// for proper matrix formatting and better readability
|
||||||
|
#define YES true
|
||||||
|
#define NO_ false
|
||||||
|
static const int success_matrix[COUNT_OF(types)][COUNT_OF(values)] = {
|
||||||
|
// types:
|
||||||
|
{YES, YES, YES, YES, YES, YES, YES}, // any
|
||||||
|
{NO_, NO_, YES, NO_, NO_, NO_, NO_}, // array
|
||||||
|
{NO_, NO_, YES, YES, NO_, NO_, NO_}, // obj
|
||||||
|
{NO_, NO_, NO_, NO_, NO_, NO_, NO_}, // fn
|
||||||
|
{NO_, YES, NO_, NO_, NO_, NO_, NO_}, // ptr
|
||||||
|
{NO_, NO_, NO_, NO_, YES, NO_, NO_}, // int32
|
||||||
|
{NO_, NO_, NO_, NO_, YES, NO_, NO_}, // double
|
||||||
|
{NO_, NO_, NO_, NO_, NO_, YES, NO_}, // str
|
||||||
|
{NO_, NO_, NO_, NO_, NO_, NO_, YES}, // bool
|
||||||
|
//und ptr arr obj num str bool <- values
|
||||||
|
};
|
||||||
|
#undef NO_
|
||||||
|
#undef YES
|
||||||
|
|
||||||
|
for(size_t i = 0; i < COUNT_OF(types); i++) {
|
||||||
|
for(size_t j = 0; j < COUNT_OF(values); j++) {
|
||||||
|
const JsValueDeclaration declaration = {
|
||||||
|
.type = types[i],
|
||||||
|
.permit_null = false,
|
||||||
|
.n_children = 0,
|
||||||
|
};
|
||||||
|
// we only care about the status, not the result. double has the largest size out of
|
||||||
|
// all the results
|
||||||
|
uint8_t result[sizeof(double)];
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &values[j], result);
|
||||||
|
if((status == JsValueParseStatusOk) != success_matrix[i][j]) {
|
||||||
|
FURI_LOG_E(TAG, "type %zu, value %zu", i, j);
|
||||||
|
mu_fail("see serial logs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_literal(struct mjs* mjs) {
|
||||||
|
static const JsValueType types[] = {
|
||||||
|
JsValueTypeAny,
|
||||||
|
JsValueTypeAnyArray,
|
||||||
|
JsValueTypeAnyObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
mjs_val_t values[] = {
|
||||||
|
mjs_mk_undefined(),
|
||||||
|
mjs_mk_array(mjs),
|
||||||
|
mjs_mk_object(mjs),
|
||||||
|
};
|
||||||
|
|
||||||
|
mu_assert_int_eq(COUNT_OF(types), COUNT_OF(values));
|
||||||
|
for(size_t i = 0; i < COUNT_OF(types); i++) {
|
||||||
|
const JsValueDeclaration declaration = {
|
||||||
|
.type = types[i],
|
||||||
|
.permit_null = false,
|
||||||
|
.n_children = 0,
|
||||||
|
};
|
||||||
|
mjs_val_t result;
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &values[i], &result);
|
||||||
|
mu_assert_int_eq(JsValueParseStatusOk, status);
|
||||||
|
mu_assert(result == values[i], "wrong result");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_primitive(
|
||||||
|
struct mjs* mjs,
|
||||||
|
JsValueType type,
|
||||||
|
const void* c_value,
|
||||||
|
size_t c_value_size,
|
||||||
|
mjs_val_t js_val) {
|
||||||
|
const JsValueDeclaration declaration = {
|
||||||
|
.type = type,
|
||||||
|
.permit_null = false,
|
||||||
|
.n_children = 0,
|
||||||
|
};
|
||||||
|
uint8_t result[c_value_size];
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &js_val, result);
|
||||||
|
mu_assert_int_eq(JsValueParseStatusOk, status);
|
||||||
|
if(type == JsValueTypeString) {
|
||||||
|
const char* result_str = *(const char**)&result;
|
||||||
|
mu_assert_string_eq(c_value, result_str);
|
||||||
|
} else {
|
||||||
|
mu_assert_mem_eq(c_value, result, c_value_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_primitives(struct mjs* mjs) {
|
||||||
|
int32_t i32 = 123;
|
||||||
|
js_value_test_primitive(mjs, JsValueTypeInt32, &i32, sizeof(i32), mjs_mk_number(mjs, i32));
|
||||||
|
|
||||||
|
double dbl = 123.456;
|
||||||
|
js_value_test_primitive(mjs, JsValueTypeDouble, &dbl, sizeof(dbl), mjs_mk_number(mjs, dbl));
|
||||||
|
|
||||||
|
const char* str = "test";
|
||||||
|
js_value_test_primitive(
|
||||||
|
mjs, JsValueTypeString, str, strlen(str) + 1, mjs_mk_string(mjs, str, ~0, false));
|
||||||
|
|
||||||
|
bool boolean = true;
|
||||||
|
js_value_test_primitive(
|
||||||
|
mjs, JsValueTypeBool, &boolean, sizeof(boolean), mjs_mk_boolean(mjs, boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t
|
||||||
|
js_value_test_enum(struct mjs* mjs, const JsValueDeclaration* decl, const char* value) {
|
||||||
|
mjs_val_t str = mjs_mk_string(mjs, value, ~0, false);
|
||||||
|
uint32_t result;
|
||||||
|
furi_check(decl->enum_size == sizeof(result));
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(mjs, decl, JsValueParseFlagNone, &status, &str, &result);
|
||||||
|
if(status != JsValueParseStatusOk) return 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_enums(struct mjs* mjs) {
|
||||||
|
static const JsValueDeclaration enum_1_variants[] = {
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 1", 1),
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 2", 2),
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 3", 3),
|
||||||
|
};
|
||||||
|
static const JsValueDeclaration enum_1 = {
|
||||||
|
.type = JsValueTypeEnum,
|
||||||
|
.enum_size = sizeof(uint32_t),
|
||||||
|
JS_VALUE_CHILDREN(enum_1_variants),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const JsValueDeclaration enum_2_variants[] = {
|
||||||
|
JS_VALUE_ENUM_VARIANT("read", 4),
|
||||||
|
JS_VALUE_ENUM_VARIANT("write", 8),
|
||||||
|
};
|
||||||
|
static const JsValueDeclaration enum_2 = {
|
||||||
|
.type = JsValueTypeEnum,
|
||||||
|
.enum_size = sizeof(uint32_t),
|
||||||
|
JS_VALUE_CHILDREN(enum_2_variants),
|
||||||
|
};
|
||||||
|
|
||||||
|
mu_assert_int_eq(1, js_value_test_enum(mjs, &enum_1, "variant 1"));
|
||||||
|
mu_assert_int_eq(2, js_value_test_enum(mjs, &enum_1, "variant 2"));
|
||||||
|
mu_assert_int_eq(3, js_value_test_enum(mjs, &enum_1, "variant 3"));
|
||||||
|
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_1, "not a thing"));
|
||||||
|
|
||||||
|
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 1"));
|
||||||
|
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 2"));
|
||||||
|
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 3"));
|
||||||
|
mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "not a thing"));
|
||||||
|
mu_assert_int_eq(4, js_value_test_enum(mjs, &enum_2, "read"));
|
||||||
|
mu_assert_int_eq(8, js_value_test_enum(mjs, &enum_2, "write"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_object(struct mjs* mjs) {
|
||||||
|
static const JsValueDeclaration enum_variants[] = {
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 1", 1),
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 2", 2),
|
||||||
|
JS_VALUE_ENUM_VARIANT("variant 3", 3),
|
||||||
|
};
|
||||||
|
static const JsValueDeclaration fields[] = {
|
||||||
|
{.type = JsValueTypeInt32, .object_field_name = "int"},
|
||||||
|
{.type = JsValueTypeString, .object_field_name = "str"},
|
||||||
|
{.type = JsValueTypeEnum,
|
||||||
|
.object_field_name = "enum",
|
||||||
|
.enum_size = sizeof(uint32_t),
|
||||||
|
JS_VALUE_CHILDREN(enum_variants)}};
|
||||||
|
static const JsValueDeclaration object_decl = {
|
||||||
|
.type = JsValueTypeObject,
|
||||||
|
JS_VALUE_CHILDREN(fields),
|
||||||
|
};
|
||||||
|
|
||||||
|
mjs_val_t object = mjs_mk_object(mjs);
|
||||||
|
JS_ASSIGN_MULTI(mjs, object) {
|
||||||
|
JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false));
|
||||||
|
JS_FIELD("int", mjs_mk_number(mjs, 123));
|
||||||
|
JS_FIELD("enum", mjs_mk_string(mjs, "variant 2", ~0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* result_str;
|
||||||
|
int32_t result_int;
|
||||||
|
uint32_t result_enum;
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(
|
||||||
|
mjs,
|
||||||
|
&object_decl,
|
||||||
|
JsValueParseFlagNone,
|
||||||
|
&status,
|
||||||
|
&object,
|
||||||
|
&result_int,
|
||||||
|
&result_str,
|
||||||
|
&result_enum);
|
||||||
|
mu_assert_int_eq(JsValueParseStatusOk, status);
|
||||||
|
mu_assert_string_eq("Helloooo!", result_str);
|
||||||
|
mu_assert_int_eq(123, result_int);
|
||||||
|
mu_assert_int_eq(2, result_enum);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void js_value_test_default(struct mjs* mjs) {
|
||||||
|
static const JsValueDeclaration fields[] = {
|
||||||
|
{.type = JsValueTypeInt32,
|
||||||
|
.permit_null = true,
|
||||||
|
.default_value = {.int32_val = 123},
|
||||||
|
.object_field_name = "int"},
|
||||||
|
{.type = JsValueTypeString, .object_field_name = "str"},
|
||||||
|
};
|
||||||
|
static const JsValueDeclaration object_decl = {
|
||||||
|
.type = JsValueTypeObject,
|
||||||
|
JS_VALUE_CHILDREN(fields),
|
||||||
|
};
|
||||||
|
|
||||||
|
mjs_val_t object = mjs_mk_object(mjs);
|
||||||
|
JS_ASSIGN_MULTI(mjs, object) {
|
||||||
|
JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false));
|
||||||
|
JS_FIELD("int", mjs_mk_undefined());
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* result_str;
|
||||||
|
int32_t result_int;
|
||||||
|
JsValueParseStatus status;
|
||||||
|
JS_VALUE_PARSE(
|
||||||
|
mjs, &object_decl, JsValueParseFlagNone, &status, &object, &result_int, &result_str);
|
||||||
|
mu_assert_string_eq("Helloooo!", result_str);
|
||||||
|
mu_assert_int_eq(123, result_int);
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST(js_value_test) {
|
||||||
|
struct mjs* mjs = mjs_create(NULL);
|
||||||
|
|
||||||
|
js_value_test_compatibility_matrix(mjs);
|
||||||
|
js_value_test_literal(mjs);
|
||||||
|
js_value_test_primitives(mjs);
|
||||||
|
js_value_test_enums(mjs);
|
||||||
|
js_value_test_object(mjs);
|
||||||
|
js_value_test_default(mjs);
|
||||||
|
|
||||||
|
mjs_destroy(mjs);
|
||||||
|
}
|
||||||
|
|
||||||
MU_TEST_SUITE(test_js) {
|
MU_TEST_SUITE(test_js) {
|
||||||
|
MU_RUN_TEST(js_value_test);
|
||||||
MU_RUN_TEST(js_test_basic);
|
MU_RUN_TEST(js_test_basic);
|
||||||
MU_RUN_TEST(js_test_math);
|
MU_RUN_TEST(js_test_math);
|
||||||
MU_RUN_TEST(js_test_event_loop);
|
MU_RUN_TEST(js_test_event_loop);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <rpc/rpc_i.h>
|
#include <rpc/rpc_i.h>
|
||||||
#include <flipper.pb.h>
|
#include <flipper.pb.h>
|
||||||
#include <applications/system/js_app/js_thread.h>
|
#include <applications/system/js_app/js_thread.h>
|
||||||
|
#include <applications/system/js_app/js_value.h>
|
||||||
|
|
||||||
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
||||||
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
|
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
|
||||||
@@ -38,4 +39,16 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
|
|||||||
JsThread*,
|
JsThread*,
|
||||||
(const char* script_path, JsThreadCallback callback, void* context)),
|
(const char* script_path, JsThreadCallback callback, void* context)),
|
||||||
API_METHOD(js_thread_stop, void, (JsThread * worker)),
|
API_METHOD(js_thread_stop, void, (JsThread * worker)),
|
||||||
|
API_METHOD(js_value_buffer_size, size_t, (const JsValueDeclaration* declaration)),
|
||||||
|
API_METHOD(
|
||||||
|
js_value_parse,
|
||||||
|
JsValueParseStatus,
|
||||||
|
(struct mjs * mjs,
|
||||||
|
const JsValueDeclaration* declaration,
|
||||||
|
JsValueParseFlag flags,
|
||||||
|
mjs_val_t* buffer,
|
||||||
|
size_t buf_size,
|
||||||
|
mjs_val_t* source,
|
||||||
|
size_t n_c_vals,
|
||||||
|
...)),
|
||||||
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ App(
|
|||||||
"js_app.c",
|
"js_app.c",
|
||||||
"js_modules.c",
|
"js_modules.c",
|
||||||
"js_thread.c",
|
"js_thread.c",
|
||||||
|
"js_value.c",
|
||||||
"plugin_api/app_api_table.cpp",
|
"plugin_api/app_api_table.cpp",
|
||||||
"views/console_view.c",
|
"views/console_view.c",
|
||||||
"modules/js_flipper.c",
|
"modules/js_flipper.c",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "js_thread_i.h"
|
#include "js_thread_i.h"
|
||||||
|
#include "js_value.h"
|
||||||
#include <flipper_application/flipper_application.h>
|
#include <flipper_application/flipper_application.h>
|
||||||
#include <flipper_application/plugins/plugin_manager.h>
|
#include <flipper_application/plugins/plugin_manager.h>
|
||||||
#include <flipper_application/plugins/composite_resolver.h>
|
#include <flipper_application/plugins/composite_resolver.h>
|
||||||
|
|||||||
274
applications/system/js_app/js_value.c
Normal file
274
applications/system/js_app/js_value.c
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
#include "js_value.h"
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
size_t js_value_buffer_size(const JsValueDeclaration* declaration) {
|
||||||
|
JsValueType type = declaration->type;
|
||||||
|
|
||||||
|
if(type == JsValueTypeString) return 1;
|
||||||
|
|
||||||
|
if(type == JsValueTypeObject) {
|
||||||
|
size_t total = 0;
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
total += js_value_buffer_size(&declaration->children[i]);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t js_value_resulting_c_values_count(const JsValueDeclaration* declaration) {
|
||||||
|
JsValueType type = declaration->type;
|
||||||
|
|
||||||
|
if(type == JsValueTypeObject) {
|
||||||
|
size_t total = 0;
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
total += js_value_resulting_c_values_count(&declaration->children[i]);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool js_value_declaration_valid(const JsValueDeclaration* declaration) {
|
||||||
|
JsValueType type = declaration->type;
|
||||||
|
|
||||||
|
// Args can have an arbitrary number of children of arbitrary types
|
||||||
|
if(type == JsValueTypeArgs) {
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++)
|
||||||
|
if(!js_value_declaration_valid(&declaration->children[i])) return false;
|
||||||
|
if(declaration->permit_null) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enums can only have EnumValue children
|
||||||
|
if(type == JsValueTypeEnum) {
|
||||||
|
if(declaration->enum_size != 1 && declaration->enum_size != 2 &&
|
||||||
|
declaration->enum_size != 4)
|
||||||
|
return false;
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
const JsValueDeclaration* child = &declaration->children[i];
|
||||||
|
if(!js_value_declaration_valid(child) || child->type != JsValueTypeEnumValue)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects must have valid children
|
||||||
|
if(type == JsValueTypeObject) {
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
const JsValueDeclaration* child = &declaration->children[i];
|
||||||
|
if(!js_value_declaration_valid(child) || !child->object_field_name) return false;
|
||||||
|
}
|
||||||
|
if(declaration->permit_null) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumValues must have their string field set
|
||||||
|
if(type == JsValueTypeEnumValue) {
|
||||||
|
return declaration->n_children == 0 && declaration->enum_string_value != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal types can't have default values
|
||||||
|
if(type == JsValueTypeAny || type == JsValueTypeAnyArray || type == JsValueTypeAnyObject ||
|
||||||
|
type == JsValueTypeFunction) {
|
||||||
|
if(declaration->permit_null) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other types can't have children
|
||||||
|
return declaration->n_children == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PREPEND_JS_ERROR_AND_RETURN(mjs, flags, ...) \
|
||||||
|
do { \
|
||||||
|
if((flags) & JsValueParseFlagReturnOnError) \
|
||||||
|
mjs_prepend_errorf((mjs), MJS_BAD_ARGS_ERROR, __VA_ARGS__); \
|
||||||
|
return JsValueParseStatusJsError; \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
static void js_value_assign_enum_val(va_list* out_pointers, size_t enum_size, uint32_t value) {
|
||||||
|
if(enum_size == 1)
|
||||||
|
*va_arg(*out_pointers, uint8_t*) = value;
|
||||||
|
else if(enum_size == 2)
|
||||||
|
*va_arg(*out_pointers, uint16_t*) = value;
|
||||||
|
else if(enum_size == 4)
|
||||||
|
*va_arg(*out_pointers, uint32_t*) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool js_value_is_null_or_undefined(mjs_val_t* val_ptr) {
|
||||||
|
return mjs_is_null(*val_ptr) || mjs_is_undefined(*val_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool js_value_maybe_assign_default(
|
||||||
|
const JsValueDeclaration* declaration,
|
||||||
|
mjs_val_t* val_ptr,
|
||||||
|
void* destination,
|
||||||
|
size_t size) {
|
||||||
|
if(declaration->permit_null && js_value_is_null_or_undefined(val_ptr)) {
|
||||||
|
memcpy(destination, &declaration->default_value, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsValueParseStatus js_value_parse_va(
|
||||||
|
struct mjs* mjs,
|
||||||
|
const JsValueDeclaration* declaration,
|
||||||
|
JsValueParseFlag flags,
|
||||||
|
mjs_val_t* source,
|
||||||
|
mjs_val_t* buffer,
|
||||||
|
size_t* buffer_index,
|
||||||
|
va_list* out_pointers) {
|
||||||
|
switch(declaration->type) {
|
||||||
|
// Literal terms
|
||||||
|
case JsValueTypeAny: {
|
||||||
|
*va_arg(*out_pointers, mjs_val_t*) = *source;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeAnyArray: {
|
||||||
|
if(!mjs_is_array(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected array");
|
||||||
|
*va_arg(*out_pointers, mjs_val_t*) = *source;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeAnyObject: {
|
||||||
|
if(!mjs_is_object(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected object");
|
||||||
|
*va_arg(*out_pointers, mjs_val_t*) = *source;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeFunction: {
|
||||||
|
if(!mjs_is_function(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected function");
|
||||||
|
*va_arg(*out_pointers, mjs_val_t*) = *source;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primitive types
|
||||||
|
case JsValueTypeRawPointer: {
|
||||||
|
void** destination = *va_arg(*out_pointers, void**);
|
||||||
|
if(js_value_maybe_assign_default(declaration, source, destination, sizeof(void*))) break;
|
||||||
|
if(!mjs_is_foreign(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected pointer");
|
||||||
|
*destination = mjs_get_ptr(mjs, *source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeInt32: {
|
||||||
|
int32_t* destination = va_arg(*out_pointers, int32_t*);
|
||||||
|
if(js_value_maybe_assign_default(declaration, source, destination, sizeof(int32_t))) break;
|
||||||
|
if(!mjs_is_number(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected number");
|
||||||
|
*destination = mjs_get_int32(mjs, *source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeDouble: {
|
||||||
|
double* destination = va_arg(*out_pointers, double*);
|
||||||
|
if(js_value_maybe_assign_default(declaration, source, destination, sizeof(double))) break;
|
||||||
|
if(!mjs_is_number(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected number");
|
||||||
|
*destination = mjs_get_double(mjs, *source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeBool: {
|
||||||
|
bool* destination = va_arg(*out_pointers, bool*);
|
||||||
|
if(js_value_maybe_assign_default(declaration, source, destination, sizeof(bool))) break;
|
||||||
|
if(!mjs_is_boolean(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected bool");
|
||||||
|
*destination = mjs_get_bool(mjs, *source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeString: {
|
||||||
|
const char** destination = va_arg(*out_pointers, const char**);
|
||||||
|
if(js_value_maybe_assign_default(declaration, source, destination, sizeof(const char*)))
|
||||||
|
break;
|
||||||
|
if(!mjs_is_string(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected string");
|
||||||
|
buffer[*buffer_index] = *source;
|
||||||
|
*destination = mjs_get_string(mjs, &buffer[*buffer_index], NULL);
|
||||||
|
(*buffer_index)++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types with children
|
||||||
|
case JsValueTypeArgs: {
|
||||||
|
furi_check(source == JS_VAL_PARSE_SOURCE_ARGS);
|
||||||
|
size_t args_provided = mjs_nargs(mjs);
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
mjs_val_t arg = (i < args_provided) ? mjs_arg(mjs, i) : MJS_UNDEFINED;
|
||||||
|
JsValueParseStatus status = js_value_parse_va(
|
||||||
|
mjs, declaration, flags, &arg, buffer, buffer_index, out_pointers);
|
||||||
|
if(status != JsValueParseStatusOk)
|
||||||
|
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "argument %zu: ", i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case JsValueTypeEnum: {
|
||||||
|
if(declaration->permit_null && js_value_is_null_or_undefined(source)) {
|
||||||
|
js_value_assign_enum_val(
|
||||||
|
out_pointers, declaration->enum_size, declaration->default_value.enum_val);
|
||||||
|
} else if(mjs_is_string(*source)) {
|
||||||
|
const char* str = mjs_get_string(mjs, source, NULL);
|
||||||
|
furi_check(str);
|
||||||
|
bool match_found = false;
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
const JsValueDeclaration* child = &declaration->children[i];
|
||||||
|
furi_check(child->type == JsValueTypeEnumValue);
|
||||||
|
if(strcmp(str, child->enum_string_value) == 0) {
|
||||||
|
js_value_assign_enum_val(
|
||||||
|
out_pointers, declaration->enum_size, child->enum_value);
|
||||||
|
match_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!match_found)
|
||||||
|
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected one of permitted strings");
|
||||||
|
} else {
|
||||||
|
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected string");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case JsValueTypeObject: {
|
||||||
|
if(!mjs_is_object(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected object");
|
||||||
|
for(size_t i = 0; i < declaration->n_children; i++) {
|
||||||
|
const JsValueDeclaration* child = &declaration->children[i];
|
||||||
|
mjs_val_t field = mjs_get(mjs, *source, child->object_field_name, ~0);
|
||||||
|
JsValueParseStatus status =
|
||||||
|
js_value_parse_va(mjs, child, flags, &field, buffer, buffer_index, out_pointers);
|
||||||
|
if(status != JsValueParseStatusOk)
|
||||||
|
PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "field %s: ", child->object_field_name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case JsValueTypeEnumValue:
|
||||||
|
furi_crash();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsValueParseStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsValueParseStatus js_value_parse(
|
||||||
|
struct mjs* mjs,
|
||||||
|
const JsValueDeclaration* declaration,
|
||||||
|
JsValueParseFlag flags,
|
||||||
|
mjs_val_t* buffer,
|
||||||
|
size_t buf_size,
|
||||||
|
mjs_val_t* source,
|
||||||
|
size_t n_c_vals,
|
||||||
|
...) {
|
||||||
|
furi_check(mjs);
|
||||||
|
furi_check(declaration);
|
||||||
|
furi_check(buffer);
|
||||||
|
|
||||||
|
// These are asserts and not checks because argument parsing has to be fast.
|
||||||
|
// People bitbang I2C from JS.
|
||||||
|
furi_assert(js_value_declaration_valid(declaration));
|
||||||
|
furi_assert(buf_size == js_value_buffer_size(declaration));
|
||||||
|
furi_assert(n_c_vals == js_value_resulting_c_values_count(declaration));
|
||||||
|
|
||||||
|
va_list out_pointers;
|
||||||
|
va_start(out_pointers, n_c_vals);
|
||||||
|
|
||||||
|
size_t buffer_index = 0;
|
||||||
|
JsValueParseStatus status =
|
||||||
|
js_value_parse_va(mjs, declaration, flags, source, buffer, &buffer_index, &out_pointers);
|
||||||
|
furi_check(buffer_index <= buf_size);
|
||||||
|
|
||||||
|
va_end(out_pointers);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
136
applications/system/js_app/js_value.h
Normal file
136
applications/system/js_app/js_value.h
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include "js_modules.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsValueTypeArgs, //<! Function arguments
|
||||||
|
JsValueTypeAny, //<! Literal term
|
||||||
|
JsValueTypeAnyArray, //<! Literal term, after ensuring that it's an array
|
||||||
|
JsValueTypeAnyObject, //<! Literal term, after ensuring that it's an object
|
||||||
|
JsValueTypeFunction, //<! Literal term, after ensuring that it's a function
|
||||||
|
JsValueTypeRawPointer, //<! Unchecked `void*`
|
||||||
|
JsValueTypeInt32, //<! Number cast to `int32_t`
|
||||||
|
JsValueTypeDouble, //<! Number cast to `double`
|
||||||
|
JsValueTypeString, //<! Any string cast to `const char*`
|
||||||
|
JsValueTypeBool, //<! Bool cast to `bool`
|
||||||
|
JsValueTypeEnum, //<! String with predefined possible values cast to the desired C enum
|
||||||
|
JsValueTypeObject, //<! Object with predefined recursive fields cast to several C values
|
||||||
|
|
||||||
|
JsValueTypeEnumValue, //<! String-to-number enum mapping
|
||||||
|
} JsValueType;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
void* ptr_val;
|
||||||
|
int32_t int32_val;
|
||||||
|
double double_val;
|
||||||
|
const char* str_val;
|
||||||
|
uint32_t enum_val;
|
||||||
|
bool bool_val;
|
||||||
|
} JsValueDefaultValue;
|
||||||
|
|
||||||
|
typedef struct JsValueDeclaration {
|
||||||
|
JsValueType type;
|
||||||
|
union {
|
||||||
|
const char* object_field_name;
|
||||||
|
const char* enum_string_value;
|
||||||
|
};
|
||||||
|
bool permit_null;
|
||||||
|
JsValueDefaultValue default_value;
|
||||||
|
union {
|
||||||
|
uint8_t enum_size;
|
||||||
|
uint32_t enum_value;
|
||||||
|
};
|
||||||
|
size_t n_children;
|
||||||
|
const struct JsValueDeclaration* children;
|
||||||
|
} JsValueDeclaration;
|
||||||
|
|
||||||
|
#define JS_VALUE_ENUM_VARIANT(str_value, int_value) \
|
||||||
|
{.type = JsValueTypeEnumValue, .enum_string_value = str_value, .enum_value = int_value}
|
||||||
|
|
||||||
|
#define JS_VALUE_CHILDREN(variants_or_fields) \
|
||||||
|
.n_children = COUNT_OF(variants_or_fields), .children = variants_or_fields
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsValueParseFlagNone = 0,
|
||||||
|
JsValueParseFlagReturnOnError =
|
||||||
|
(1
|
||||||
|
<< 0), //<! Sets mjs error string to a description of the parsing error and returns from the JS function
|
||||||
|
} JsValueParseFlag;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsValueParseStatusOk, //<! Parsing completed successfully
|
||||||
|
JsValueParseStatusJsError, //<! Parsing failed due to incorrect JS input
|
||||||
|
} JsValueParseStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Magic value that tells `js_value_parse` to source the JS values from
|
||||||
|
* function arguments.
|
||||||
|
*/
|
||||||
|
#define JS_VAL_PARSE_SOURCE_ARGS NULL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Determines the size of the buffer array of `mjs_val_t`s that needs to
|
||||||
|
* be passed to `js_value_parse`.
|
||||||
|
*/
|
||||||
|
size_t js_value_buffer_size(const JsValueDeclaration* declaration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Converts a JS value into a series of C values.
|
||||||
|
*
|
||||||
|
* @param[in] mjs mJS instance pointer
|
||||||
|
* @param[in] declaration Type declaration for the input value
|
||||||
|
* @param[in] flags See the corresponding enum
|
||||||
|
* @param[out] buffer Temporary buffer for values that need to live
|
||||||
|
* longer than the function call. To determine the
|
||||||
|
* size of the buffer, use `js_value_buffer_size`.
|
||||||
|
* Values parsed by this function will become invalid
|
||||||
|
* when this buffer goes out of scope.
|
||||||
|
* @param[in] buf_size Number of entries in the temporary buffer (i.e.
|
||||||
|
* `COUNT_OF`, not `sizeof`).
|
||||||
|
* @param[in] source Source JS value that needs to be converted. Set to
|
||||||
|
* `JS_VAL_PARSE_SOURCE_ARGS` to fetch the values from
|
||||||
|
* arguments.
|
||||||
|
* @param[in] n_c_vals Number of output C values
|
||||||
|
* @param[out] ... Pointers to output C values. The order in which
|
||||||
|
* these values are populated corresponds to the order
|
||||||
|
* in which the values are defined in the declaration.
|
||||||
|
*
|
||||||
|
* @returns Parsing status
|
||||||
|
*/
|
||||||
|
JsValueParseStatus js_value_parse(
|
||||||
|
struct mjs* mjs,
|
||||||
|
const JsValueDeclaration* declaration,
|
||||||
|
JsValueParseFlag flags,
|
||||||
|
mjs_val_t* buffer,
|
||||||
|
size_t buf_size,
|
||||||
|
mjs_val_t* source,
|
||||||
|
size_t n_c_vals,
|
||||||
|
...);
|
||||||
|
|
||||||
|
#define JS_VALUE_PARSE(mjs, declaration, flags, status_ptr, value_ptr, ...) \
|
||||||
|
void* _args[] = {__VA_ARGS__}; \
|
||||||
|
size_t _n_args = COUNT_OF(_args); \
|
||||||
|
size_t _temp_buf_len = js_value_buffer_size(declaration); \
|
||||||
|
mjs_val_t _temp_buffer[_temp_buf_len]; \
|
||||||
|
*(status_ptr) = js_value_parse( \
|
||||||
|
mjs, declaration, flags, _temp_buffer, _temp_buf_len, value_ptr, _n_args, __VA_ARGS__);
|
||||||
|
|
||||||
|
#define JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, declaration, ...) \
|
||||||
|
JsValueParseStatus _status; \
|
||||||
|
JS_VALUE_PARSE( \
|
||||||
|
mjs, \
|
||||||
|
declaration, \
|
||||||
|
JsValueParseFlagReturnOnError, \
|
||||||
|
&_status, \
|
||||||
|
JS_VAL_PARSE_SOURCE_ARGS, \
|
||||||
|
__VA_ARGS__); \
|
||||||
|
if(_status != JsValueParseStatusOk) return;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user