diff --git a/api/catter-c/capi.d.ts b/api/catter-c/capi.d.ts index 0ad035b..543da7a 100644 --- a/api/catter-c/capi.d.ts +++ b/api/catter-c/capi.d.ts @@ -1,60 +1,85 @@ export {}; -/** - * Action to apply to a captured command. - * - * Possible values: - * - `"skip"`: Ignore the command in catter, but still execute the original command. - * - `"drop"`: Skip execution of the original command. - * - `"abort"`: Abort the whole execution and report an error. - * - `"modify"`: Replace the original command with a modified command. - */ -export type ActionType = "skip" | "drop" | "abort" | "modify"; - /** * Result returned from a command handler. */ -export type Action = { - /** - * Replacement command data used when the action type is `"modify"`. - */ - data?: CommandData; - - /** - * Action to apply to the captured command. - */ - type: ActionType; -}; +export type Action = + | { + /** + * Ignore the command in catter, but still execute the original command. + */ + type: "skip"; + } + | { + /** + * Skip execution of the original command. + */ + type: "drop"; + } + | { + /** + * Abort the whole execution and report an error. + */ + type: "abort"; + } + | { + /** + * Replace the original command with a modified command. + */ + type: "modify"; + + /** + * Replacement command data for the modified command. + */ + data: CommandData; + }; /** - * Execution event kind. + * Action discriminator extracted from {@link Action}. */ -export type EventType = "finish" | "output"; +export type ActionType = Action["type"]; /** * Event emitted while a command is executing. */ -export type ExecutionEvent = { - /** - * Standard output content for an `"output"` event. - */ - stdout?: string; - - /** - * Standard error content for an `"output"` event. - */ - stderr?: string; +export type ExecutionEvent = + | { + /** + * Event category. + */ + type: "output"; + + /** + * Standard output content for an `"output"` event. + */ + stdout: string; + + /** + * Standard error content for an `"output"` event. + */ + stderr: string; + + /** + * Runtime-defined status code for this output event. + */ + code: number; + } + | { + /** + * Event category. + */ + type: "finish"; + + /** + * Final process exit code. + */ + code: number; + }; - /** - * Process exit code. For non-finished output events, this is runtime-defined. - */ - code: number; - - /** - * Event category. - */ - type: EventType; -}; +/** + * Execution event discriminator extracted from {@link ExecutionEvent}. + */ +export type EventType = ExecutionEvent["type"]; /** * Runtime capabilities exposed to the script. @@ -172,12 +197,39 @@ export type CommandData = { parent?: number; }; +/** + * Tagged command capture result passed to `service_on_command`. + */ +export type CommandCaptureResult = + | { + /** + * Indicates command capture succeeded. + */ + success: true; + + /** + * Captured command payload. + */ + data: CommandData; + } + | { + /** + * Indicates command capture failed. + */ + success: false; + + /** + * Failure details for command capture. + */ + error: CatterErr; + }; + export function service_on_start( cb: (config: CatterConfig) => CatterConfig, ): void; export function service_on_finish(cb: (event: ExecutionEvent) => void): void; export function service_on_command( - cb: (id: number, data: CommandData | CatterErr) => Action, + cb: (id: number, data: CommandCaptureResult) => Action, ): void; export function service_on_execution( cb: (id: number, event: ExecutionEvent) => void, diff --git a/api/src/scripts/cdb.ts b/api/src/scripts/cdb.ts index 459b910..cea7a3e 100644 --- a/api/src/scripts/cdb.ts +++ b/api/src/scripts/cdb.ts @@ -81,16 +81,14 @@ export class CDB implements service.CatterService { }); } - onCommand( - id: number, - data: service.CommandData | service.CatterErr, - ): service.Action { - if ("msg" in data) { - io.println(`CDB received error: ${data.msg}`); + onCommand(id: number, data: service.CommandCaptureResult): service.Action { + if (!data.success) { + io.println(`CDB received error: ${data.error.msg}`); } else { - const compiler = identify_compiler(data.exe); + const command = data.data; + const compiler = identify_compiler(command.exe); if (compiler !== "unknown") { - this.commandArray.push([compiler, data]); + this.commandArray.push([compiler, command]); } } return { diff --git a/api/src/service.ts b/api/src/service.ts index 8fe24bc..b1955a6 100644 --- a/api/src/service.ts +++ b/api/src/service.ts @@ -11,6 +11,7 @@ export type { CatterConfig, CatterErr, CatterRuntime, + CommandCaptureResult, CommandData, EventType, ExecutionEvent, @@ -20,10 +21,9 @@ import type { Action, ActionType, CatterConfig, - CatterErr, CatterRuntime, - CommandData, EventType, + CommandCaptureResult, ExecutionEvent, } from "catter-c"; @@ -37,9 +37,12 @@ import type { * const kind = ActionKind[0]; // "skip" * ``` */ -export const ActionKind = ["skip", "drop", "abort", "modify"] as const; - -const _ActionKindTypeCheck: (typeof ActionKind)[number] = {} as ActionType; +export const ActionKind = [ + "skip", + "drop", + "abort", + "modify", +] as const satisfies readonly ActionType[]; /** * Supported execution event kinds. @@ -51,9 +54,10 @@ const _ActionKindTypeCheck: (typeof ActionKind)[number] = {} as ActionType; * const isOutputEvent = EventKind.includes("output"); * ``` */ -export const EventKind = ["finish", "output"] as const; - -const _EventKindTypeCheck: (typeof EventKind)[number] = {} as EventType; +export const EventKind = [ + "finish", + "output", +] as const satisfies readonly EventType[]; /** * Callback group for subscribing to catter lifecycle and command events. @@ -96,9 +100,9 @@ export interface CatterService { * Called when catter captures a command. * * @param id - Unique command identifier that can be correlated with execution events. - * @param data - Captured command data, or a {@link CatterErr} when capture fails. + * @param data - Tagged command capture result with a `success` discriminator. */ - onCommand: (id: number, data: CommandData | CatterErr) => Action; + onCommand: (id: number, data: CommandCaptureResult) => Action; /** * Called when a captured command emits execution events. @@ -148,20 +152,26 @@ export function onFinish(cb: (event: ExecutionEvent) => void): void { /** * Registers a callback that handles each captured command. * - * @param cb - Callback invoked for each command. The first argument is the stable command ID, and the second is either the captured command payload or a capture error. + * @param cb - Callback invoked for each command. The first argument is the stable command ID, and the second is a tagged capture result (`success: true` for command data, `success: false` for capture errors). * * @example * ```typescript * onCommand((id, data) => { - * if ("msg" in data) { + * if (!data.success) { * return { type: "skip" }; * } - * return { type: "modify", data: { ...data, argv: [...data.argv, "--verbose"] } }; + * return { + * type: "modify", + * data: { + * ...data.data, + * argv: [...data.data.argv, "--verbose"], + * }, + * }; * }); * ``` */ export function onCommand( - cb: (id: number, data: CommandData | CatterErr) => Action, + cb: (id: number, data: CommandCaptureResult) => Action, ): void { service_on_command(cb); } diff --git a/api/test/service.ts b/api/test/service.ts index 8f3ff1c..e6192ef 100644 --- a/api/test/service.ts +++ b/api/test/service.ts @@ -34,23 +34,23 @@ service.register({ onCommand(id, data) { debug.assertThrow(id === 7); - if ("msg" in data) { - debug.assertThrow(data.msg === "spawn failed"); + if (!data.success) { + debug.assertThrow(data.error.msg === "spawn failed"); commandErrorBranchSeen = true; return { type: "skip" }; } - debug.assertThrow(data.cwd === "/tmp"); - debug.assertThrow(data.exe === "clang++"); - debug.assertThrow(data.argv.length === 3); - debug.assertThrow(data.argv[2] === "-c"); - debug.assertThrow(data.parent === 41); + debug.assertThrow(data.data.cwd === "/tmp"); + debug.assertThrow(data.data.exe === "clang++"); + debug.assertThrow(data.data.argv.length === 3); + debug.assertThrow(data.data.argv[2] === "-c"); + debug.assertThrow(data.data.parent === 41); return { type: "modify", data: { - ...data, - argv: [...data.argv, serviceArg], + ...data.data, + argv: [...data.data.argv, serviceArg], }, }; }, diff --git a/src/catter/core/capi/type.h b/src/catter/core/capi/type.h index 789e874..a9e6e5b 100644 --- a/src/catter/core/capi/type.h +++ b/src/catter/core/capi/type.h @@ -1,25 +1,132 @@ #pragma once #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include "qjs.h" +#include "util/enum.h" namespace catter::js { +namespace detail { +template +struct Bridge; +template +T make_reflected_object(qjs::Object object); + +template +qjs::Object to_reflected_object(JSContext* ctx, const T& value); +} // namespace detail + +template + requires std::is_enum_v> +struct Tag { + bool operator== (const Tag& other) const = default; +}; + +template +concept EnumValues = + (std::is_enum_v> && ...) && + (std::same_as, std::decay_t> && ...); + +template + requires EnumValues +struct TaggedUnion; + +#define TAG \ + template <> \ + struct Tag + +template + requires EnumValues +struct TaggedUnion : public std::variant...> { + using TagType = std::common_type_t...>; + using std::variant...>::variant; + TaggedUnion() = default; + TaggedUnion(const TaggedUnion&) = default; + TaggedUnion(TaggedUnion&&) = default; + TaggedUnion& operator= (const TaggedUnion&) = default; + TaggedUnion& operator= (TaggedUnion&&) = default; + + static TaggedUnion make(qjs::Object object) { + return detail::Bridge::from_js(qjs::Value::from(object)); + } + + qjs::Object to_object(JSContext* ctx) const { + return detail::Bridge::to_js(ctx, *this); + } + + std::variant...>& variant() { + return static_cast...>&>(*this); + } + + const std::variant...>& variant() const { + return static_cast...>&>(*this); + } + + template + decltype(auto) visit(V&& visitor) const { + return std::visit(std::forward(visitor), this->variant()); + } + + template + decltype(auto) visit(V&& visitor) { + return std::visit(std::forward(visitor), this->variant()); + } + + TagType type() const { + return visit([](const Tag&) -> TagType { return E; }); + } + + template + decltype(auto) get_if() { + return std::get_if>(&this->variant()); + } + + template + decltype(auto) get_if() const { + return std::get_if>(&this->variant()); + } + + template + decltype(auto) get() { + return std::get>(this->variant()); + } + + template + decltype(auto) get() const { + return std::get>(this->variant()); + } + + bool operator== (const TaggedUnion& other) const { + return this->visit([&](const T& tag) -> bool { + return other.visit([&](const U& other_tag) -> bool { + if constexpr(std::is_same_v) { + return tag == other_tag; + } else { + return false; + } + }); + }); + } +}; + namespace detail { namespace et = eventide; template -E enum_value(std::string_view name) { +constexpr E enum_value(std::string_view name) { if(auto val = et::refl::enum_value(name); val.has_value()) { return *val; } @@ -27,7 +134,7 @@ E enum_value(std::string_view name) { } template -std::string_view enum_name(E value) { +constexpr std::string_view enum_name(E value) { return et::refl::enum_name(value, "unknown"); } @@ -52,87 +159,107 @@ std::vector enum_names(const std::vector& values) { } template -struct is_optional : std::false_type {}; +struct property_name_mapper { + constexpr static std::string_view map(std::string_view field_name) { + return field_name; + } +}; template -struct is_optional> : std::true_type { - using value_type = T; +struct Bridge { + static T from_js(const qjs::Value& value) { + return value.as(); + } + + static auto to_js(JSContext* ctx, const T& value) { + return qjs::Value::from(ctx, value); + } }; template -constexpr inline bool is_optional_v = is_optional::value; + requires et::refl::reflectable_class +struct Bridge { + static T from_js(const qjs::Value& value) { + return make_reflected_object(value.as()); + } + + static auto to_js(JSContext* ctx, const T& value) { + return to_reflected_object(ctx, value); + } +}; template -struct is_vector : std::false_type {}; + requires std::is_enum_v +struct Bridge { + static T from_js(const qjs::Value& value) { + return enum_value(value.as()); + } -template -struct is_vector> : std::true_type { - using value_type = T; + static auto to_js(JSContext* ctx, const T& value) { + return qjs::Value::from(ctx, std::string(enum_name(value))); + } }; template -constexpr inline bool is_vector_v = is_vector::value; + requires std::is_enum_v +struct Bridge> { + static std::vector from_js(const qjs::Value& value) { + auto names = value.as>().as>(); + return enum_values(names); + } -template -concept ReflectableObject = et::refl::reflectable_class; + static auto to_js(JSContext* ctx, const std::vector& vec) { + return qjs::Array::from(ctx, enum_names(vec)); + } +}; template -struct property_name_mapper { - constexpr static std::string_view map(std::string_view field_name) { - return field_name; +struct Bridge> { + static std::vector from_js(const qjs::Value& value) { + return value.as>().template as>(); + } + + static auto to_js(JSContext* ctx, const std::vector& vec) { + return qjs::Array::from(ctx, vec); } }; -template -T make_reflected_object(qjs::Object object); +template + requires EnumValues +struct Bridge> { + using Union = TaggedUnion; -template -qjs::Object to_reflected_object(JSContext* ctx, const T& value); + static Union from_js(const qjs::Value& value) { + auto object = value.as(); -template -auto to_property_value(JSContext* ctx, const T& value) { - if constexpr(std::is_enum_v) { - return qjs::Value::from(ctx, std::string(enum_name(value))); - } else if constexpr(is_vector_v && std::is_enum_v::value_type>) { - return qjs::Array::from(ctx, enum_names(value)); - } else if constexpr(is_vector_v && - std::same_as::value_type, std::string>) { - return qjs::Array::from(ctx, value); - } else if constexpr(ReflectableObject) { - return to_reflected_object(ctx, value); - } else { - return qjs::Value::from(ctx, value); + auto tag = object["type"].as(); + + return dispatch(tag, [&](in_place_enum) -> Union { + return make_reflected_object>(object); + }); } -} -template -T from_property_value(const qjs::Value& value) { - if constexpr(std::is_enum_v) { - return enum_value(value.as()); - } else if constexpr(is_vector_v && std::is_enum_v::value_type>) { - auto names = value.as>().as>(); - return enum_values::value_type>(names); - } else if constexpr(is_vector_v && - std::same_as::value_type, std::string>) { - return value.as>().as>(); - } else if constexpr(ReflectableObject) { - return make_reflected_object(value.as()); - } else { - return value.as(); + static auto to_js(JSContext* ctx, const Union& union_value) { + return union_value.visit([&](const Tag& tag) { + auto object = to_reflected_object(ctx, tag); + object.set_property("type", std::string(enum_name(E))); + return object; + }); } -} +}; template T read_property(const qjs::Object& object, std::string_view property_name) { - if constexpr(is_optional_v) { - using value_type = typename is_optional::value_type; - auto optional_value = object.get_optional_property(std::string(property_name)); - if(!optional_value.has_value()) { + if constexpr(et::is_optional_v) { + using value_type = typename T::value_type; + auto prop_val = object[std::string(property_name)]; + if(!prop_val.is_undefined()) { + return Bridge::from_js(prop_val); + } else { return std::nullopt; } - return from_property_value(*optional_value); } else { - return from_property_value(object[std::string(property_name)]); + return Bridge::from_js(object[std::string(property_name)]); } } @@ -141,12 +268,13 @@ void write_property(qjs::Object& object, std::string_view property_name, JSContext* ctx, const T& value) { - if constexpr(is_optional_v) { + if constexpr(et::is_optional_v) { + using value_type = typename T::value_type; if(value.has_value()) { - object.set_property(std::string(property_name), to_property_value(ctx, value.value())); + object.set_property(std::string(property_name), Bridge::to_js(ctx, *value)); } } else { - object.set_property(std::string(property_name), to_property_value(ctx, value)); + object.set_property(std::string(property_name), Bridge::to_js(ctx, value)); } } @@ -266,42 +394,30 @@ struct CatterErr { std::string msg; }; -struct Action { - static Action make(qjs::Object object) { - return detail::make_reflected_object(std::move(object)); - } +using Action = + TaggedUnion; - qjs::Object to_object(JSContext* ctx) const { - return detail::to_reflected_object(ctx, *this); - } - - bool operator== (const Action&) const = default; +using ExecutionEvent = TaggedUnion; -public: - std::optional data; - ActionType type; +TAG { + CommandData data; + bool operator== (const Tag& other) const = default; }; -struct ExecutionEvent { - static ExecutionEvent make(qjs::Object object) { - return detail::make_reflected_object(std::move(object)); - } - - qjs::Object to_object(JSContext* ctx) const { - return detail::to_reflected_object(ctx, *this); - } - - bool operator== (const ExecutionEvent&) const = default; +TAG { + std::string stdOut; + std::string stdErr; + int64_t code; + bool operator== (const Tag& other) const = default; +}; -public: - std::optional stdOut; - std::optional stdErr; +TAG { int64_t code; - EventType type; + bool operator== (const Tag& other) const = default; }; template <> -struct detail::property_name_mapper { +struct detail::property_name_mapper> { constexpr static std::string_view map(std::string_view field_name) { if(field_name == "stdOut") { return "stdout"; @@ -313,4 +429,5 @@ struct detail::property_name_mapper { } }; +#undef TAG } // namespace catter::js diff --git a/src/catter/core/ipc.cc b/src/catter/core/ipc.cc index 1243e0e..16e4ce0 100644 --- a/src/catter/core/ipc.cc +++ b/src/catter/core/ipc.cc @@ -16,6 +16,7 @@ #include "ipc.h" +#include "util/enum.h" #include "util/log.h" #include "util/serde.h" #include "util/data.h" @@ -79,29 +80,22 @@ struct Dispatcher { using ReflRequest = eventide::refl::reflection; Request req = Serde::deserialize(buf_reader); LOG_INFO("Handling request of type: {}", eventide::refl::enum_name(req)); - return [&](this const auto& self) -> std::optional> { - if constexpr(I < ReflRequest::member_count) { - constexpr auto val = ReflRequest::member_values[I]; - if(val == req) { - constexpr auto mem_fn = match_mem_fn(); - if constexpr(mem_fn == nullptr) { - LOG_INFO("No matching member function found for request type: {}", - eventide::refl::enum_name(req)); - throw std::runtime_error( - std::format("No matching member function found for request type: {}", - eventide::refl::enum_name(req))); - } else { - return Helper>::call(eventide::bind_ref(obj), - buf_reader); - } + + return catter::dispatch( + req, + [&](in_place_enum) -> std::optional> { + constexpr auto mem_fn = match_mem_fn(); + if constexpr(mem_fn == nullptr) { + LOG_INFO("No matching member function found for request type: {}", + eventide::refl::enum_name(req)); + throw std::runtime_error( + std::format("No matching member function found for request type: {}", + eventide::refl::enum_name(req))); + } else { + return Helper>::call(eventide::bind_ref(obj), + buf_reader); } - return self.template operator()(); - } else { - throw std::runtime_error( - std::format("Unknown request type received: {}", - static_cast>(req))); - } - }(); + }); } }; diff --git a/src/catter/core/js.cc b/src/catter/core/js.cc index 4aacce3..edc91c8 100644 --- a/src/catter/core/js.cc +++ b/src/catter/core/js.cc @@ -49,13 +49,19 @@ void on_finish(ExecutionEvent event) { return self.on_finish(event.to_object(self.on_finish.context())); } -Action on_command(uint32_t id, std::variant data) { +Action on_command(uint32_t id, std::expected data) { if(!self.on_command) { throw std::runtime_error("service.onCommand is not registered"); } - return Action::make(self.on_command( - id, - std::visit([](const auto& v) { return v.to_object(self.on_command.context()); }, data))); + auto command_result = qjs::Object::empty_one(self.on_command.context()); + if(data.has_value()) { + command_result.set_property("success", true); + command_result.set_property("data", data->to_object(self.on_command.context())); + } else { + command_result.set_property("success", false); + command_result.set_property("error", data.error().to_object(self.on_command.context())); + } + return Action::make(self.on_command(id, std::move(command_result))); } void on_execution(uint32_t id, ExecutionEvent event) { diff --git a/src/catter/core/js.h b/src/catter/core/js.h index dbffea9..e0adbff 100644 --- a/src/catter/core/js.h +++ b/src/catter/core/js.h @@ -1,6 +1,6 @@ #pragma once +#include #include -#include #include "qjs.h" @@ -39,7 +39,7 @@ void set_on_execution(qjs::Object cb); CatterConfig on_start(CatterConfig config); void on_finish(ExecutionEvent event); -Action on_command(uint32_t id, std::variant data); +Action on_command(uint32_t id, std::expected data); void on_execution(uint32_t id, ExecutionEvent event); }; // namespace catter::js diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index 457fb17..9d699f3 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -154,6 +154,14 @@ class Value { return detail::value_trans>::from(std::forward(value)); } + static Value undefined(JSContext* ctx) noexcept { + return Value{ctx, JS_UNDEFINED}; + } + + static Value null(JSContext* ctx) noexcept { + return Value{ctx, JS_NULL}; + } + template std::optional to() const noexcept { return detail::value_trans::to(*this); diff --git a/src/catter/main.cc b/src/catter/main.cc index 5eed97a..53716a1 100644 --- a/src/catter/main.cc +++ b/src/catter/main.cc @@ -49,24 +49,22 @@ class ServiceImpl : public ipc::InjectService { .parent = this->parent_id, }); - switch(act.type) { + switch(act.type()) { case js::ActionType::drop: case js::ActionType::skip: { return data::action{.type = data::action::INJECT, .cmd = cmd}; } case js::ActionType::modify: { - if(!act.data.has_value()) { - throw std::runtime_error("Modify action must have data"); - } + auto tag = act.get(); return data::action{ .type = data::action::INJECT, .cmd = { - .cwd = std::move(act.data->cwd), - .executable = std::move(act.data->exe), - .args = std::move(act.data->argv), - .env = std::move(act.data->env), + .cwd = std::move(tag.data.cwd), + .executable = std::move(tag.data.exe), + .args = std::move(tag.data.argv), + .env = std::move(tag.data.env), } }; } @@ -75,15 +73,11 @@ class ServiceImpl : public ipc::InjectService { } void finish(int64_t code) override { - js::on_execution(this->id, - { - .code = code, - .type = js::EventType::finish, - }); + js::on_execution(this->id, js::Tag{.code = code}); } void report_error(data::ipcid_t parent_id, std::string error_msg) override { - js::on_command(id, js::CatterErr{.msg = error_msg}); + js::on_command(id, std::unexpected(js::CatterErr{.msg = std::move(error_msg)})); } struct Factory { @@ -163,7 +157,7 @@ void inject(const Config& config) { catter::js::run_js_file(content, config.script_path); } - js::on_start({ + auto new_config = js::on_start({ .scriptPath = config.script_path, .scriptArgs = config.script_args, .buildSystemCommand = config.build_system_command, @@ -177,11 +171,10 @@ void inject(const Config& config) { Session session; - auto ret = session.run(config.build_system_command, ServiceImpl::Factory{}); + auto ret = session.run(new_config.buildSystemCommand, ServiceImpl::Factory{}); - js::on_finish({ + js::on_finish(js::Tag{ .code = ret, - .type = js::EventType::finish, }); } diff --git a/src/common/util/enum.h b/src/common/util/enum.h new file mode 100644 index 0000000..adab4ce --- /dev/null +++ b/src/common/util/enum.h @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace catter { + +template + requires std::is_enum_v> +struct in_place_enum { + using type = std::decay_t; + constexpr static type value = E; +}; + +template + requires std::is_enum_v && std::is_invocable_v> +auto dispatch(E e, F&& f) { + using Enum = eventide::refl::reflection; + using R = std::invoke_result_t>; + using Callback = R(F && f); + + struct Data { + E value; + Callback* callback; + }; + + constexpr auto map = [](std::index_sequence) { + return std::to_array({ + {Enum::member_values[I], [](F&& f) -> R { + return std::invoke(std::forward(f), in_place_enum{}); + }} + ... + }); + }(std::make_index_sequence{}); + using U = std::underlying_type_t; + const auto target = static_cast(e); + std::size_t left = 0; + std::size_t right = Enum::member_values.size(); + while(left < right) { + const auto mid = left + (right - left) / 2; + if(static_cast(Enum::member_values[mid]) < target) { + left = mid + 1; + } else { + right = mid; + } + } + if(left < Enum::member_values.size() && static_cast(Enum::member_values[left]) == target) { + return map[left].callback(std::forward(f)); + } + throw std::runtime_error("Invalid enum value"); +} + +template + requires std::is_enum_v && std::is_invocable_v> +auto dispatch(std::string_view e, F&& f) { + using Enum = eventide::refl::reflection; + using R = std::invoke_result_t>; + using Callback = R(F && f); + + struct Data { + std::string_view name; + Callback* callback; + }; + + constexpr auto map = [](std::index_sequence) { + return std::to_array({ + {Enum::member_names[I], [](F&& f) -> R { + return std::invoke(std::forward(f), in_place_enum{}); + }} + ... + }); + }(std::make_index_sequence{}); + + for(const auto& entry: map) { + if(entry.name == e) { + return entry.callback(std::forward(f)); + } + } + throw std::runtime_error("Invalid enum name"); +} +} // namespace catter diff --git a/tests/unit/catter/core/js.cc b/tests/unit/catter/core/js.cc index ac9a366..0349353 100644 --- a/tests/unit/catter/core/js.cc +++ b/tests/unit/catter/core/js.cc @@ -13,13 +13,15 @@ #include namespace fs = std::filesystem; +using namespace catter; +using namespace catter::js; namespace { void ensure_qjs_initialized(const fs::path& js_path) { static bool initialized = false; if(!initialized) { - catter::js::init_qjs({.pwd = js_path}); + js::init_qjs({.pwd = js_path}); initialized = true; } } @@ -33,17 +35,17 @@ void run_js_file_by_name(const fs::path& js_path, std::string_view file_name) { } std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - catter::js::run_js_file(content, full_path.string()); + js::run_js_file(content, full_path.string()); } void run_basic_js_case(std::string_view file_name, bool with_fs_test_env = false) { try { - auto js_path = fs::path(catter::config::data::js_test_path.data()); + auto js_path = fs::path(config::data::js_test_path.data()); ensure_qjs_initialized(js_path); if(with_fs_test_env) { - auto js_path_res = fs::path(catter::config::data::js_test_res_path.data()); - catter::TempFileManager manager(js_path_res / "fs-test-env"); + auto js_path_res = fs::path(config::data::js_test_res_path.data()); + TempFileManager manager(js_path_res / "fs-test-env"); std::error_code ec; manager.create("a/tmp.txt", ec, "Alpha!\nBeta!\nKid A;\nend;"); @@ -68,8 +70,8 @@ void run_basic_js_case(std::string_view file_name, bool with_fs_test_env = false } run_js_file_by_name(js_path, file_name); - } catch(catter::qjs::Exception& ex) { - catter::output::redLn("{}", ex.what()); + } catch(qjs::Exception& ex) { + output::redLn("{}", ex.what()); throw ex; } } @@ -103,21 +105,21 @@ TEST_SUITE(js_tests) { TEST_CASE(run_service_js_file_and_callbacks) { auto f = [&]() { - auto js_path = fs::path(catter::config::data::js_test_path.data()); + auto js_path = fs::path(config::data::js_test_path.data()); ensure_qjs_initialized(js_path); run_js_file_by_name(js_path, "service.js"); - catter::js::CatterRuntime runtime{ - .supportActions = {catter::js::ActionType::skip, - catter::js::ActionType::drop, - catter::js::ActionType::abort, - catter::js::ActionType::modify}, - .supportEvents = {catter::js::EventType::finish, catter::js::EventType::output}, - .type = catter::js::CatterRuntime::Type::inject, + js::CatterRuntime runtime{ + .supportActions = {js::ActionType::skip, + js::ActionType::drop, + js::ActionType::abort, + js::ActionType::modify}, + .supportEvents = {js::EventType::finish, js::EventType::output}, + .type = js::CatterRuntime::Type::inject, .supportParentId = true, }; - catter::js::CatterConfig config{ + js::CatterConfig config{ .scriptPath = "script.ts", .scriptArgs = {"--input", "compile_commands.json"}, .buildSystemCommand = {"xmake", "build"}, @@ -126,14 +128,14 @@ TEST_SUITE(js_tests) { .isScriptSupported = true, }; - auto updated_config = catter::js::on_start(config); + auto updated_config = js::on_start(config); EXPECT_TRUE(updated_config.scriptPath == config.scriptPath); EXPECT_TRUE(updated_config.scriptArgs.size() == 3); EXPECT_TRUE(updated_config.scriptArgs.back() == "--from-service"); EXPECT_TRUE(updated_config.options.log == false); EXPECT_TRUE(updated_config.isScriptSupported == false); - catter::js::CommandData data{ + js::CommandData data{ .cwd = "/tmp", .exe = "clang++", .argv = {"clang++", "main.cc", "-c"}, @@ -142,36 +144,35 @@ TEST_SUITE(js_tests) { .parent = 41, }; - auto action = catter::js::on_command(7, data); - EXPECT_TRUE(action.type == catter::js::ActionType::modify); - EXPECT_TRUE(action.data.has_value()); - EXPECT_TRUE(action.data->argv.size() == 4); - EXPECT_TRUE(action.data->argv.back() == "--from-service"); - EXPECT_TRUE(action.data->parent.has_value()); - EXPECT_TRUE(action.data->parent.value() == 41); - - catter::js::CatterErr err{.msg = "spawn failed"}; - auto error_action = catter::js::on_command(7, err); - EXPECT_TRUE(error_action.type == catter::js::ActionType::skip); - EXPECT_TRUE(!error_action.data.has_value()); - - catter::js::ExecutionEvent output_event{ - .stdOut = std::string{"hello from stdout"}, - .stdErr = std::string{"hello from stderr"}, + auto action = js::on_command(7, data); + action.visit([&](const Tag& tag) { + if constexpr(E == js::ActionType::modify) { + EXPECT_TRUE(tag.data.argv.size() == 4); + EXPECT_TRUE(tag.data.argv.back() == "--from-service"); + EXPECT_TRUE(tag.data.parent.has_value()); + EXPECT_TRUE(tag.data.parent.value() == 41); + } else { + EXPECT_TRUE(E == js::ActionType::modify); + } + }); + + js::CatterErr err{.msg = "spawn failed"}; + auto error_action = js::on_command(7, std::unexpected(err)); + EXPECT_TRUE(error_action.type() == js::ActionType::skip); + + js::ExecutionEvent output_event = js::Tag{ + .stdOut = "hello from stdout", + .stdErr = "hello from stderr", .code = 0, - .type = catter::js::EventType::output, }; - catter::js::on_execution(7, output_event); + js::on_execution(7, output_event); - catter::js::ExecutionEvent finish_event{ - .stdOut = std::nullopt, - .stdErr = std::nullopt, + js::ExecutionEvent finish_event = js::Tag{ .code = 0, - .type = catter::js::EventType::finish, }; - catter::js::on_execution(7, finish_event); + js::on_execution(7, finish_event); - catter::js::on_finish(finish_event); + js::on_finish(finish_event); }; EXPECT_NOTHROWS(f()); @@ -187,14 +188,13 @@ TEST_SUITE(js_tests) { TEST_CASE(run_js_file_reports_async_error_message_and_stack) { auto f = [&]() { - auto js_path = fs::path(catter::config::data::js_test_path.data()); + auto js_path = fs::path(config::data::js_test_path.data()); ensure_qjs_initialized(js_path); bool caught = false; try { - catter::js::run_js_file("await Promise.reject(new Error('async boom'));\n", - "reject.js"); - } catch(const catter::qjs::Exception& ex) { + js::run_js_file("await Promise.reject(new Error('async boom'));\n", "reject.js"); + } catch(const qjs::Exception& ex) { caught = true; std::string message = ex.what(); EXPECT_TRUE(message.contains("async boom")); diff --git a/tests/unit/catter/core/type.cc b/tests/unit/catter/core/type.cc index 30be6b9..45315e1 100644 --- a/tests/unit/catter/core/type.cc +++ b/tests/unit/catter/core/type.cc @@ -7,6 +7,7 @@ #include "capi/type.h" using namespace catter; +using namespace catter::js; namespace { @@ -24,10 +25,10 @@ TEST_SUITE(api_tests) { auto runtime = qjs::Runtime::create(); auto& ctx = runtime.context(); - catter::js::CatterRuntime catter_runtime{ - .supportActions = {catter::js::ActionType::skip, catter::js::ActionType::modify}, - .supportEvents = {catter::js::EventType::finish}, - .type = catter::js::CatterRuntime::Type::inject, + js::CatterRuntime catter_runtime{ + .supportActions = {js::ActionType::skip, js::ActionType::modify}, + .supportEvents = {js::EventType::finish}, + .type = js::CatterRuntime::Type::inject, .supportParentId = true }; @@ -42,24 +43,23 @@ TEST_SUITE(api_tests) { auto runtime = qjs::Runtime::create(); auto& ctx = runtime.context(); - catter::js::CommandData command_data{ + js::CommandData command_data{ .cwd = "D:/Code/hook/catter", .exe = "clang++", .argv = {"clang++", "main.cc", "-c"}, .env = {"CC=clang++", "CATTER_LOG=1"}, - .runtime = {.supportActions = {catter::js::ActionType::skip, - catter::js::ActionType::modify}, - .supportEvents = {catter::js::EventType::finish, - catter::js::EventType::output}, - .type = catter::js::CatterRuntime::Type::env, + .runtime = {.supportActions = {js::ActionType::skip, js::ActionType::modify}, + .supportEvents = {js::EventType::finish, js::EventType::output}, + .type = js::CatterRuntime::Type::env, .supportParentId = true}, .parent = 42 }; - catter::js::Action modify_action{.data = command_data, - .type = catter::js::ActionType::modify}; - catter::js::Action skip_action{.data = std::nullopt, - .type = catter::js::ActionType::skip}; + Action modify_action = Tag{.data = command_data}; + + Action skip_action = Tag{ + + }; EXPECT_TRUE(is_roundtrip_equal(ctx, command_data)); EXPECT_TRUE(is_roundtrip_equal(ctx, modify_action)); @@ -74,23 +74,22 @@ TEST_SUITE(api_tests) { auto runtime = qjs::Runtime::create(); auto& ctx = runtime.context(); - catter::js::ExecutionEvent output_event{.stdOut = std::string{"hello"}, - .stdErr = std::string{"warn"}, - .code = 0, - .type = catter::js::EventType::output}; - catter::js::ExecutionEvent finish_event{.stdOut = std::nullopt, - .stdErr = std::nullopt, - .code = 1, - .type = catter::js::EventType::finish}; + js::ExecutionEvent output_event = js::Tag{ + .stdOut = "hello", + .stdErr = "warn", + .code = 0, + }; + js::ExecutionEvent finish_event = js::Tag{ + .code = 1, + }; - catter::js::CatterConfig config{ + js::CatterConfig config{ .scriptPath = "scripts/demo.js", .scriptArgs = {"--input", "compile_commands.json"}, .buildSystemCommand = {"xmake", "build"}, - .runtime = {.supportActions = {catter::js::ActionType::drop, - catter::js::ActionType::abort}, - .supportEvents = {catter::js::EventType::finish}, - .type = catter::js::CatterRuntime::Type::inject, + .runtime = {.supportActions = {js::ActionType::drop, js::ActionType::abort}, + .supportEvents = {js::EventType::finish}, + .type = js::CatterRuntime::Type::inject, .supportParentId = false}, .options = {.log = true}, .isScriptSupported = true