From 300e907ac8f228ecd1c5e1589778e776dd9baba6 Mon Sep 17 00:00:00 2001 From: Darwin <5746693+darwin67@users.noreply.github.com> Date: Sat, 4 Nov 2023 20:05:10 -1000 Subject: [PATCH] Change approach for SDK (#59) resolves #58 Change the execution model to match the other SDKs. The current attempt is not flexible at all. Also update it to follow TS SDK v3 convention. - execute function - step run - step sleep - step sleep_until - step wait_for_event - step send_event --- .github/workflows/ci.yml | 7 +- Makefile | 2 +- dev/event.ex | 63 +- dev/scheduled/cron.ex | 11 - lib/inngest/event.ex | 5 +- lib/inngest/function.ex | 569 ++------------- lib/inngest/function/handler.ex | 118 +-- lib/inngest/function/input.ex | 38 + lib/inngest/function/op.ex | 36 +- lib/inngest/function/step.ex | 4 - lib/inngest/router/invoke.ex | 86 ++- lib/inngest/router/register.ex | 2 +- lib/inngest/step_tool.ex | 148 ++++ lib/inngest/trigger.ex | 8 +- mix.exs | 4 +- test/inngest/function/handler_test.exs | 958 ++++++++++++------------- test/inngest/function_test.exs | 20 +- test/inngest/router/helper_test.exs | 7 +- test/support/funcs.ex | 164 +++-- 19 files changed, 951 insertions(+), 1299 deletions(-) delete mode 100644 dev/scheduled/cron.ex create mode 100644 lib/inngest/function/input.ex create mode 100644 lib/inngest/step_tool.ex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0633574..3bd59e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,11 +38,16 @@ jobs: run: make deps - name: Run tests with coverage - run: mix coveralls.github + run: mix coveralls.json env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + formatter: name: Formatter runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 9bd3a30..33daa72 100644 --- a/Makefile +++ b/Makefile @@ -40,4 +40,4 @@ changelog: .PHONY: inngest-dev inngest-dev: - inngest-cli dev -u http://127.0.0.1:4000/api/inngest + inngest-cli dev -v -u http://127.0.0.1:4000/api/inngest diff --git a/dev/event.ex b/dev/event.ex index b607129..dd0dbf7 100644 --- a/dev/event.ex +++ b/dev/event.ex @@ -1,43 +1,46 @@ -defmodule Inngest.Dev.EventFn do +defmodule Inngest.Dev.EventFn2 do @moduledoc false - use Inngest.Function, - name: "test func", - event: "test/event" + use Inngest.Function + alias Inngest.{FnOpts, Trigger} - # batch_events: %{max_size: 3, timeout: "10s"} + @func %FnOpts{id: "test-func-v2", name: "test func v2"} + @trigger %Trigger{event: "test/hello"} - run "test 1st run" do - {:ok, %{run: "do something"}} - end + @impl true + def exec(ctx, %{run_id: run_id, step: step} = _args) do + IO.inspect("First log") - step "test 1st step" do - {:ok, %{hello: "world"}} - end + greet = + step.run(ctx, "hello", fn -> + "Hello world" + end) + |> IO.inspect() - sleep "2s" + step.sleep(ctx, "sleep-test", "10s") + # step.sleep(ctx, "sleep-until-test", "2023-11-05T00:12:00Z") - step "test 2nd step" do - {:ok, 100} - end + IO.inspect("Second log") - sleep "2s" - # sleep "until 1m later" do - # "2023-07-18T07:31:00Z" - # end - - step "test 3rd - state accumulate" do - {:ok, %{result: "ok"}} - end + # step.wait_for_event(ctx, "wait-test", %{ + # event: "test/yolo", + # timeout: "1h" + # # match: "data.foo" + # }) + # |> IO.inspect() - # wait_for_event "test/wait" do - # match = "data.yo" - # [timeout: "1d", if: "event.#{match} == async.#{match}"] - # end + step.send_event(ctx, "test-event-sending", %{ + name: "test/foobar", + data: %{"foo" => "bar"} + }) + |> IO.inspect() - # wait_for_event "test/wait", do: [timeout: "1d", match: "data.yo"] + name = + step.run(ctx, "name", fn -> + "John Doe" + end) + |> IO.inspect() - run "result", %{data: data} do - {:ok, data} + {:ok, "#{greet} #{name}"} |> IO.inspect() end end diff --git a/dev/scheduled/cron.ex b/dev/scheduled/cron.ex deleted file mode 100644 index 1623ef5..0000000 --- a/dev/scheduled/cron.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Inngest.Dev.CronFn do - @moduledoc false - - use Inngest.Function, - name: "test cron", - cron: "TZ=America/Los_Angeles * * * * *" - - step "show current time" do - {:ok, %{time: Timex.now()}} - end -end diff --git a/lib/inngest/event.ex b/lib/inngest/event.ex index c82d02a..435502b 100644 --- a/lib/inngest/event.ex +++ b/lib/inngest/event.ex @@ -82,6 +82,9 @@ end defimpl Jason.Encoder, for: Inngest.Event do def encode(value, opts) do - Jason.Encode.map(Map.from_struct(value), opts) + value + |> Map.from_struct() + |> Map.drop([:datetime]) + |> Jason.Encode.map(opts) end end diff --git a/lib/inngest/function.ex b/lib/inngest/function.ex index 6589101..dd2f1f5 100644 --- a/lib/inngest/function.ex +++ b/lib/inngest/function.ex @@ -7,18 +7,18 @@ defmodule Inngest.Function do the necessary attributes and handlers it needs to work with Inngest. defmodule MyApp.Inngest.SomeJob do - use Inngest.Function, - id: "my-function", # optional - name: "some job", # required - event_name: "job/foobar", # required - batch_events: %{max_size: 10, timeout: "3s"} # optional + use Inngest.Function + alias Inngest.{FnOpts, Trigger} + + @func %FnOpts{id: "my-func", name: "some job"} + @trigger %Trigger{event: "job/foobar"} end - ## Function configuration + ## Function Options - The `Inngest.Function` macro accepts the following options. + The `Inngest.FnOpts` accepts the following attributes. - #### `id` - `string` (optional) + #### `id` - `string` (required) A unique identifier for your function to override the default name. Also known in technical terms, a `slug`. @@ -28,24 +28,13 @@ defmodule Inngest.Function do A unique name for your function. This will be used to create a unique slug id by default if `id` is not provided. - #### `event_name` or `cron` - `string` (required) - - See [Trigger](#module-trigger) for reference. - - #### `batch_events` - `map` (optional) - - Configure how the function should consume batches of events. - Accepts a `map` with the following attributes: - - - `max_size` - `number`: The maximum number of events a batch can have. Current limit is `100` - - `timeout` - `string`: How long to wait before invoking the function with the batch even if it's not full ## Trigger - A trigger is exactlyi what the name says. It's the thing that triggers a function + A trigger is exactly what the name says. It's the thing that triggers a function to run. One of the following is required, and they're mutually exclusive. - #### `event_name` - `string` + #### `event` - `string` and/or `expression` - `string` The name of the event that will trigger this event to run. We recommend it to name it with a prefix so it's a easier pattern to identify @@ -58,40 +47,9 @@ defmodule Inngest.Function do A [unix-cron](https://crontab.guru/) compatible schedule string. Optional timezone prefix, e.g. `TZ=Europe/Paris 0 12 * * 5`. - - ## Handlers - - The handlers are your code that runs whenever the trigger occurs. - - defmodule MyApp.Inngest.SomeJob do - use Inngest.Function, - name: "some job", - event_name: "job/foobar" - - # This will run when an event `job/foobar` is received - run "do something", %{event: event, data: data} do - # do - # some - # stuff - - {:ok, %{result: result}} # returns a map as a result - end - end - - Unlike some other SDKs, the Elixir SDK follows the pattern `ExUnit` is using. - Essentially providing a DSL that wraps your logic, making them deterministic. - - All handlers can be repeated, and they will be executed in the order they're - declared: - - - `Inngest.Function.run/3` - Run code non-deterministically, executing every time the function is triggered - - `Inngest.Function.step/3` - Run code deterministically, and individually retryable. - - `Inngest.Function.sleep/1` or `Inngest.Function.sleep/3` - Sleep for a given amount of time or until a given time. - - `Inngest.Function.wait_for_event/3` - Pause a function's execution until another event is received. - """ - alias Inngest.Config - alias Inngest.Function.{Step, Trigger} + alias Inngest.{Config, Trigger} + alias Inngest.Function.{Context, Input, Step} @doc """ Returns the function's human-readable ID, such as "sign-up-flow" @@ -108,40 +66,48 @@ defmodule Inngest.Function do """ @callback trigger() :: Trigger.t() - @reserved [:run, :step, :sleep] + @doc """ + The method to be called when the Inngest function starts execution + """ + @callback exec(Context.t(), Input.t()) :: {:ok, any()} | {:error, any()} - defmacro __using__(opts) do + defmacro __using__(_opts) do quote location: :keep do - unless Inngest.Function.__register__(__MODULE__, unquote(opts)) do - # placeholder - end + alias Inngest.{Client, Trigger} + alias Inngest.Function.Step - alias Inngest.Client - alias Inngest.Function.{Trigger, Step} - # import Inngest.Function, only: [step: 2, step: 3] - import Inngest.Function - @before_compile unquote(__MODULE__) + Enum.each( + [:func, :trigger], + &Module.register_attribute(__MODULE__, &1, persist: true) + ) @behaviour Inngest.Function - @opts unquote(opts) - - @fn_slug if Keyword.get(@opts, :id), - do: Keyword.get(@opts, :id), - else: - Keyword.get(@opts, :name) - |> Slug.slugify() - @impl true - def slug(), do: @fn_slug + def slug() do + __MODULE__.__info__(:attributes) + |> Keyword.get(:func) + |> List.first() + |> Map.get(:id) + end @impl true - def name(), do: Keyword.get(@opts, :name) + def name() do + case __MODULE__.__info__(:attributes) + |> Keyword.get(:func) + |> List.first() + |> Map.get(:name) do + nil -> slug() + name -> name + end + end @impl true - def trigger(), do: @opts |> Map.new() |> trigger() - defp trigger(%{event: event} = _opts), do: %Trigger{event: event} - defp trigger(%{cron: cron} = _opts), do: %Trigger{cron: cron} + def trigger() do + __MODULE__.__info__(:attributes) + |> Keyword.get(:trigger) + |> List.first() + end def step(path), do: %{ @@ -155,415 +121,15 @@ defmodule Inngest.Function do } } - def steps(), do: __handler__().steps - def serve(path) do %{ id: slug(), name: name(), triggers: [trigger()], - batchEvents: batch_events(@opts), steps: step(path), mod: __MODULE__ } end - - defp batch_events(opts) do - with %{max_size: max_size, timeout: timeout} <- Keyword.get(opts, :batch_events, nil), - true <- is_integer(max_size), - true <- String.match?(timeout, ~r/[0-9]+s/), - {num, ""} <- String.replace(timeout, ~r/[^\d]/, "") |> Integer.parse(), - true <- num > 0 && num <= 60 do - %{maxSize: max_size, timeout: timeout} - else - _ -> nil - end - end - - def send(events) do - # NOTE: keep this for now so we can add things like tracing in the future - Client.send(events, []) - end - end - end - - def __register__(module, _opts) do - registered? = Module.has_attribute?(module, :inngest_fn_steps) - - unless registered? do - accumulate_attributes = [ - :inngest_fn_steps - ] - - Enum.each( - accumulate_attributes, - &Module.register_attribute(module, &1, accumulate: true, persist: true) - ) - end - - registered? - end - - @doc """ - Defines a normal execution block with a `message` that is non-deterministic. - - Meaning whenever Inngest asks the SDK to execute, the code block wrapped - within `run` will always run, no pun intended. - - Hence making it non deterministic, since each execution can yield a different - result. - - This is best for things that do not need idempotency. The result here will be - passed on to the next execution unit. - - #### Arguments - - It accepts an optional `map` that includes - - - `event` - - `data` - - #### Expected output types - @spec :ok | {:ok, map()} | {:error, map()} - - where the data is a `map` accumulated with outputs from previous executions. - - ## Examples - - run "non deterministic code block", %{event: event, data: data} do - # do - # something - # here - - {:ok, %{result: result}} - end - """ - defmacro run(message, var \\ quote(do: _), contents) do - unless is_tuple(var) do - IO.warn( - "step context is always a map. The pattern " <> - "#{inspect(Macro.to_string(var))} will never match", - Macro.Env.stacktrace(__CALLER__) - ) - end - - contents = - case contents do - [do: block] -> - quote do - unquote(block) - end - - _ -> - quote do - try(unquote(contents)) - end - end - - var = Macro.escape(var) - contents = Macro.escape(contents, unquote: true) - - %{module: mod, file: file, line: line} = __CALLER__ - - quote bind_quoted: [ - var: var, - contents: contents, - message: message, - mod: mod, - file: file, - line: line - ] do - slug = Inngest.Function.register_step(mod, file, line, :exec_run, message, []) - def unquote(slug)(unquote(var)), do: unquote(contents) - end - end - - @doc """ - Defines a deterministic execution block with a `message`. - - This is exactly the same as `Inngest.Function.run/3`, except the code within - the `step` blocks are always guaranteed to be executed once. - - Subsequent calls to the SDK will not execute and uses the previously executed - result. - - If the code block returns an error or raised an exception, it will be retried. - - #### Arguments - - It accepts an optional `map` that includes - - - `event` - - `data` - - #### Expected output types - @spec :ok | {:ok, map()} | {:error, map()} - - where the data is a `map` accumulated with outputs from previous executions. - - ## Examples - step "idempotent code block", %{event: event, data: data} do - # do - # something - # here - - {:ok, %{result: result}} - end - """ - defmacro step(message, var \\ quote(do: _), contents) do - unless is_tuple(var) do - IO.warn( - "step context is always a map. The pattern " <> - "#{inspect(Macro.to_string(var))} will never match", - Macro.Env.stacktrace(__CALLER__) - ) - end - - contents = - case contents do - [do: block] -> - quote do - unquote(block) - end - - _ -> - quote do - try(unquote(contents)) - end - end - - var = Macro.escape(var) - contents = Macro.escape(contents, unquote: true) - - %{module: mod, file: file, line: line} = __CALLER__ - - quote bind_quoted: [ - var: var, - contents: contents, - message: message, - mod: mod, - file: file, - line: line - ] do - slug = Inngest.Function.register_step(mod, file, line, :step_run, message) - - def unquote(slug)(unquote(var)), do: unquote(contents) - end - end - - @doc """ - Pauses the function execution until the specified DateTime. - - Expected valid datetime string formats are: - - `RFC3389` - - `RFC1123` - - `RFC882` - - `UNIX` - - `ANSIC` - - `ISOdate` - - ## Examples - sleep "sleep until 2023-10-25", %{event: event, data: data} do - # do something to caculate time - - # return the specified time that it should sleep until - "2023-07-18T07:31:00Z" - end - """ - defmacro sleep(message, var \\ quote(do: _), contents) do - unless is_tuple(var) do - IO.warn( - "step context is always a map. The pattern " <> - "#{inspect(Macro.to_string(var))} will never match", - Macro.Env.stacktrace(__CALLER__) - ) - end - - contents = - case contents do - [do: block] -> - quote do - unquote(block) - end - - _ -> - quote do - try(unquote(contents)) - end - end - - var = Macro.escape(var) - contents = Macro.escape(contents, unquote: true) - - %{module: mod, file: file, line: line} = __CALLER__ - - quote bind_quoted: [ - var: var, - contents: contents, - message: message, - mod: mod, - file: file, - line: line - ] do - slug = Inngest.Function.register_step(mod, file, line, :step_sleep, message, execute: true) - - def unquote(slug)(unquote(var)), do: unquote(contents) - end - end - - @doc """ - Set a duration to pause the execution of your function. - - Valid durations are combination of - - `s` - second - - `m` - minute - - `h` - hour - - `d` - day - - ## Examples - sleep "2s" - sleep "1d" - sleep "5m" - sleep "1h30m" - """ - defmacro sleep(duration) do - %{module: mod, file: file, line: line} = __CALLER__ - - # Add differentiator for sleeps with potentially similar duration - idx = Module.get_attribute(mod, :inngest_sleep_idx, 0) - Module.put_attribute(mod, :inngest_sleep_idx, idx + 1) - - quote bind_quoted: [duration: duration, mod: mod, file: file, line: line, idx: idx] do - slug = Inngest.Function.register_step(mod, file, line, :step_sleep, duration, idx: idx) - def unquote(slug)(), do: nil - end - end - - @doc """ - Pause function execution until a particular event is received before continuing. - - It returns the accepted event object or `nil` if the event is not received within - the timeout. - - The event name will be used as the key for storing the returned event for subsequent - execution units. - - ## Examples - - wait_for_event "auth/signup.email.confirmed", %{event: event, data: data} do - match = "user.id" - [timeout: "1d", if: "event.\#{match} == async.\#{match}"] - end - - # or in a shorter version - wait_for_event "auth/signup.email.confirmed", do: [timeout: "1d", match: "user.id"] - """ - defmacro wait_for_event(event_name, var \\ quote(do: _), contents) do - unless is_tuple(var) do - IO.warn( - "step context is always a map. The pattern " <> - "#{inspect(Macro.to_string(var))} will never match", - Macro.Env.stacktrace(__CALLER__) - ) - end - - contents = - case contents do - [do: block] -> - quote do - unquote(block) - end - - _ -> - quote do - try(unquote(contents)) - end - end - - var = Macro.escape(var) - contents = Macro.escape(contents, unquote: true) - - %{module: mod, file: file, line: line} = __CALLER__ - - quote bind_quoted: [ - var: var, - contents: contents, - event_name: event_name, - mod: mod, - file: file, - line: line - ] do - slug = Inngest.Function.register_step(mod, file, line, :step_wait_for_event, event_name) - - def unquote(slug)(unquote(var)), do: unquote(contents) - end - end - - def register_step(mod, file, line, step_type, name, tags \\ []) do - unless Module.has_attribute?(mod, :inngest_fn_steps) do - raise "cannot define #{step_type}. Please make sure you have invoked " <> - "\"use Inngest.Function\" in the current module" - end - - opts = - if step_type == :step_wait_for_event, - do: tags |> normalize_tags(), - else: %{} - - slug = - case Keyword.get(tags, :idx) do - nil -> validate_step_name("#{step_type} #{name}") - idx -> validate_step_name("#{step_type} #{name} #{idx}") - end - - if Module.defines?(mod, {slug, 1}) do - raise ~s("#{slug}" is already defined in #{inspect(mod)}) - end - - tags = - tags - |> normalize_tags() - |> validate_tags() - |> Map.merge(%{ - file: file, - line: line - }) - - fn_slug = Module.get_attribute(mod, :fn_slug) - - step = %Step{ - id: slug, - name: name, - step_type: step_type, - opts: opts, - tags: tags, - mod: mod, - runtime: %Step.RunTime{ - url: "#{Config.app_host()}/api/inngest?fnId=#{fn_slug}&step=#{slug}" - }, - retries: %Step.Retry{} - } - - Module.put_attribute(mod, :inngest_fn_steps, step) - - slug - end - - defmacro __before_compile__(env) do - steps = - env.module - |> Module.get_attribute(:inngest_fn_steps) - |> Enum.reverse() - |> Macro.escape() - - quote do - def __handler__ do - %Inngest.Function.Handler{ - file: __ENV__.file, - mod: __MODULE__, - steps: unquote(steps) - } - end end end @@ -599,43 +165,20 @@ defmodule Inngest.Function do # end def validate_datetime(_), do: {:error, "Expect valid DateTime formatted input"} +end - defp normalize_tags(tags) do - tags - |> Enum.reverse() - |> Enum.reduce(%{}, fn - {key, value}, acc -> Map.put(acc, key, value) - tag, acc when is_atom(tag) -> Map.put(acc, tag, true) - tag, acc when is_list(tag) -> Enum.into(tag, acc) - end) - end - - defp validate_tags(tags) do - for tag <- @reserved, Map.has_key?(tags, tag) do - raise "cannot set tag #{inspect(tag)} because it is reserved by Inngest.Function" - end +defmodule Inngest.FnOpts do + @moduledoc false - unless is_atom(tags[:step_type]) do - raise("value for tag \":step_type\" must be an atom") - end + defstruct [ + :id, + :name, + :retries + ] - tags - end - - defp validate_step_name(name) do - try do - name - |> String.replace(~r/(\s|\:)+/, "_") - |> String.downcase() - |> String.to_atom() - rescue - SystemLimitError -> - # credo:disable-for-next-line - raise SystemLimitError, """ - the computed name of a step (which includes its type, \ - block if present, and the step name itself) must be shorter than 255 characters, \ - got: #{inspect(name)} - """ - end - end + @type t() :: %__MODULE__{ + id: binary(), + name: binary(), + retries: number() + } end diff --git a/lib/inngest/function/handler.ex b/lib/inngest/function/handler.ex index a0193f7..111bcf6 100644 --- a/lib/inngest/function/handler.ex +++ b/lib/inngest/function/handler.ex @@ -3,121 +3,17 @@ defmodule Inngest.Function.Handler do A struct that keeps info about function, and handles the invoking of steps """ - alias Inngest.Function.{Step, UnhashedOp, GeneratorOpCode, Handler} - - defstruct [:mod, :file, :steps] - - @type t() :: %__MODULE__{ - mod: module(), - file: binary(), - steps: [Step.t()] - } + alias Inngest.Function.{UnhashedOp, GeneratorOpCode} @doc """ Handles the invoking of steps and runs from the executor """ - @spec invoke(Handler.t(), map()) :: {200 | 206 | 400 | 500, map()} - # No steps detected - def invoke(%{steps: []} = _handler, _params) do - {200, %{message: "no steps detected"}} - end - - # TODO: remove the linter ignore - # credo:disable-for-next-line - def invoke( - %{steps: steps} = _handler, - %{ - event: event, - events: events, - params: %{ - "ctx" => %{ - "stack" => %{ - "current" => _current, - "stack" => stack - } - }, - "steps" => data - } - } = _args - ) do - %{state_data: state_data, next: next} = - steps - |> Enum.reduce(%{state_data: %{}, next: nil, idx: 0}, fn step, acc -> - %{state_data: state_data, next: next, idx: idx} = acc - - case next do - nil -> - case step.step_type do - :exec_run -> - case exec(step, %{event: event, data: state_data}) do - :ok -> - acc - - {:ok, result} -> - acc - |> Map.put(:state_data, Map.merge(state_data, result)) - - {:error, _error} -> - acc - end - - _ -> - hash = - UnhashedOp.from_step(step) - |> UnhashedOp.hash() - - state = Map.get(data, hash) - - state = - if hash == Enum.at(stack, idx) do - # credo:disable-for-next-line - case step.step_type do - # credo:disable-for-next-line - :step_sleep -> - # credo:disable-for-next-line - if is_nil(state), do: %{}, else: state - - # credo:disable-for-next-line - :step_wait_for_event -> - # credo:disable-for-next-line - %{step.name => state} - - # credo:disable-for-next-line - :step_run -> - # credo:disable-for-next-line - if is_nil(state), do: %{step.name => state}, else: state - - _ -> - state - end - else - state - end - - # use step name as key if cached state is not a map value - state = - if !is_nil(state) && !is_map(state) do - %{step.name => state} - else - state - end - - next = if is_nil(state), do: step, else: nil - state = if is_nil(state), do: state_data, else: state_data |> Map.merge(state) - - acc - |> Map.put(:state_data, state) - |> Map.put(:next, next) - |> Map.put(:idx, idx + 1) - end - - _ -> - acc - end - end) - - fn_arg = %{event: event, events: events, data: state_data} - exec(next, fn_arg) + @spec invoke(Inngest.Function, map()) :: {200 | 206 | 400 | 500, map()} + def invoke(mod, args) do + case mod.run(args) do + {:ok, val} -> {200, val} + {:error, val} -> {400, val} + end end # Nothing left to run, return as completed diff --git a/lib/inngest/function/input.ex b/lib/inngest/function/input.ex new file mode 100644 index 0000000..63de72c --- /dev/null +++ b/lib/inngest/function/input.ex @@ -0,0 +1,38 @@ +defmodule Inngest.Function.Input do + @moduledoc false + + defstruct [ + :event, + :events, + :step, + :run_id, + # :logger, # TODO? + :attempt + ] + + @type t() :: %__MODULE__{ + event: Inngest.Event.t(), + events: [Inngest.Event.t()], + step: Inngest.StepTool, + run_id: binary(), + attempt: number() + } +end + +defmodule Inngest.Function.Context do + @moduledoc false + + defstruct [ + :attempt, + :run_id, + :stack, + steps: %{} + ] + + @type t() :: %__MODULE__{ + attempt: number(), + run_id: binary(), + stack: map(), + steps: map() + } +end diff --git a/lib/inngest/function/op.ex b/lib/inngest/function/op.ex index 21e3e49..8b14a78 100644 --- a/lib/inngest/function/op.ex +++ b/lib/inngest/function/op.ex @@ -2,7 +2,7 @@ defmodule Inngest.Function.UnhashedOp do @moduledoc false alias Inngest.Enums - alias Inngest.Function.Step + # alias Inngest.Function.Step defstruct [:name, :op, pos: 0, opts: %{}] @@ -13,15 +13,15 @@ defmodule Inngest.Function.UnhashedOp do opts: map() } - @spec from_step(Step.t()) :: t() - def from_step(step) do - %__MODULE__{ - name: step.name, - op: Enums.opcode(step.step_type), - pos: Map.get(step.tags, :idx, 0), - opts: step.opts - } - end + # @spec from_step(Step.t()) :: t() + # def from_step(step) do + # %__MODULE__{ + # name: step.name, + # op: Enums.opcode(step.step_type), + # pos: Map.get(step.tags, :idx, 0), + # opts: step.opts + # } + # end @spec hash(t()) :: binary() def hash(unhashedop) do @@ -35,7 +35,6 @@ defmodule Inngest.Function.GeneratorOpCode do alias Inngest.Enums - @derive Jason.Encoder defstruct [ # op represents the type of operation invoked in the function :op, @@ -45,6 +44,8 @@ defmodule Inngest.Function.GeneratorOpCode do # name represents the name of the step, or the sleep duration # for sleeps :name, + # display_name represents the display name of the step on the UI + :display_name, # opts indicate the options for the operation, e.g matching # expressions when setting up async event listeners via # `waitForEvent`, or retry policies for steps @@ -58,7 +59,18 @@ defmodule Inngest.Function.GeneratorOpCode do op: Enums.opcode(), id: binary(), name: binary(), + display_name: binary(), opts: any(), - data: map() + data: any() } end + +defimpl Jason.Encoder, for: Inngest.Function.GeneratorOpCode do + def encode(value, opts) do + value = + value + |> Map.put(:displayName, Map.get(value, :display_name)) + + Jason.Encode.map(value, opts) + end +end diff --git a/lib/inngest/function/step.ex b/lib/inngest/function/step.ex index cab1e71..afc3416 100644 --- a/lib/inngest/function/step.ex +++ b/lib/inngest/function/step.ex @@ -7,8 +7,6 @@ defmodule Inngest.Function.Step do defstruct [ :id, :name, - :step_type, - :tags, :mod, :runtime, :retries, @@ -18,9 +16,7 @@ defmodule Inngest.Function.Step do @type t() :: %__MODULE__{ id: atom(), name: binary(), - step_type: atom(), opts: map(), - tags: map(), mod: module(), runtime: RunTime, retries: Retry diff --git a/lib/inngest/router/invoke.ex b/lib/inngest/router/invoke.ex index 7077c31..5eb1848 100644 --- a/lib/inngest/router/invoke.ex +++ b/lib/inngest/router/invoke.ex @@ -4,7 +4,7 @@ defmodule Inngest.Router.Invoke do import Plug.Conn import Inngest.Router.Helper alias Inngest.{Config, Signature} - alias Inngest.Function.Handler + alias Inngest.Function.GeneratorOpCode @content_type "application/json" @@ -38,29 +38,51 @@ defmodule Inngest.Router.Invoke do %{request_path: path, private: %{raw_body: [body]}} = conn, %{"event" => event, "events" => events, "ctx" => ctx, "fnId" => fn_slug} = params ) do - funcs = + func = params |> load_functions() |> func_map(path) + |> Map.get(fn_slug) - args = %{ - ctx: ctx, - event: event, - events: events, - fn_slug: fn_slug, - funcs: funcs, - params: params + ctx = %Inngest.Function.Context{ + attempt: Map.get(ctx, "attempt", 0), + run_id: Map.get(ctx, "run_id"), + stack: Map.get(ctx, "stack"), + steps: Map.get(params, "steps") + } + + input = %Inngest.Function.Input{ + event: Inngest.Event.from(event), + events: Enum.map(events, &Inngest.Event.from/1), + run_id: Map.get(ctx, "run_id"), + step: Inngest.StepTool } {status, payload} = case Config.is_dev() do true -> - invoke(args) + {status, resp} = invoke(func, ctx, input) + + payload = + case Jason.encode(resp) do + {:ok, val} -> val + {:error, err} -> Jason.encode!(err.message) + end + + {status, payload} false -> with sig <- conn |> Plug.Conn.get_req_header("x-inngest-signature") |> List.first(), true <- Signature.signing_key_valid?(sig, Config.signing_key(), body) do - invoke(args) + {status, resp} = invoke(func, ctx, input) + + payload = + case Jason.encode(resp) do + {:ok, val} -> val + {:error, err} -> Jason.encode!(err.message) + end + + {status, payload} else _ -> {400, Jason.encode!(%{error: "unable to verify signature"})} end @@ -68,6 +90,7 @@ defmodule Inngest.Router.Invoke do conn |> put_resp_content_type(@content_type) + |> put_req_header("x-inngest-sdk", "elixir:v1") |> send_resp(status, payload) |> halt() end @@ -80,31 +103,24 @@ defmodule Inngest.Router.Invoke do # 200, resp -> execution completed (including steps) of function # 400, error -> non retriable error # 500, error -> retriable error - @spec invoke(map()) :: {200 | 206 | 400 | 500, binary()} - defp invoke( - %{ctx: ctx, event: event, events: events, fn_slug: fn_slug, funcs: funcs, params: params} = - _ - ) do - func = Map.get(funcs, fn_slug) - - args = %{ - event: Inngest.Event.from(event), - events: Enum.map(events, &Inngest.Event.from/1), - run_id: Map.get(ctx, "run_id"), - params: params - } - - {status, resp} = - func.mod.__handler__() - |> Handler.invoke(args) - - payload = - case Jason.encode(resp) do - {:ok, val} -> val - {:error, err} -> Jason.encode!(err.message) + defp invoke(func, ctx, input) do + try do + case func.mod.exec(ctx, input) do + {:ok, val} -> + {200, val} + + {:error, error} -> + {400, error} end - - {status, payload} + catch + # TODO: Panic Control + %GeneratorOpCode{} = opcode -> + {206, [opcode]} + + _ -> + # TODO: return error + {400, "error"} + end end defp fn_run_steps(run_id), do: fn_run_data("/v0/runs/#{run_id}/actions") diff --git a/lib/inngest/router/register.ex b/lib/inngest/router/register.ex index ac9ac60..38bf9de 100644 --- a/lib/inngest/router/register.ex +++ b/lib/inngest/router/register.ex @@ -49,7 +49,7 @@ defmodule Inngest.Router.Register do payload = %{ url: Config.app_host() <> path, - v: "1", + v: "0.1", deployType: "ping", sdk: Config.sdk_version(), framework: framework, diff --git a/lib/inngest/step_tool.ex b/lib/inngest/step_tool.ex new file mode 100644 index 0000000..f0ed55c --- /dev/null +++ b/lib/inngest/step_tool.ex @@ -0,0 +1,148 @@ +defmodule Inngest.StepTool do + @moduledoc false + + alias Inngest.Event + alias Inngest.Function.{Context, UnhashedOp, GeneratorOpCode} + + @type id() :: binary() + @type datetime() :: binary() | DateTime.t() | Date.t() | NaiveDateTime.t() + + @spec run(Context.t(), id(), fun()) :: any() + def run(%{steps: steps} = _ctx, step_id, func) do + op = %UnhashedOp{name: step_id, op: "Step"} + hashed_id = UnhashedOp.hash(op) + + # check for hash + case Map.get(steps, hashed_id) do + nil -> + # if not, execute function + result = func.() + + # cancel execution and return with opcode + throw(%GeneratorOpCode{ + id: hashed_id, + name: step_id, + display_name: step_id, + op: op.op, + data: result + }) + + # if found, return value + val -> + val + end + end + + @spec sleep(Context.t(), id(), binary()) :: nil + def sleep(%{steps: steps} = _ctx, step_id, duration) do + op = %UnhashedOp{name: step_id, op: "Sleep"} + hashed_id = UnhashedOp.hash(op) + + if Map.has_key?(steps, hashed_id) do + nil + else + throw(%GeneratorOpCode{ + id: hashed_id, + name: duration, + display_name: step_id, + op: op.op, + data: nil + }) + end + end + + @spec sleep_until(Context.t(), id(), datetime()) :: nil + def sleep_until(%{steps: steps} = _ctx, step_id, time) do + op = %UnhashedOp{name: step_id, op: "Sleep"} + hashed_id = UnhashedOp.hash(op) + + if Map.has_key?(steps, hashed_id) do + nil + else + case Inngest.Function.validate_datetime(time) do + {:ok, datetime} -> + throw(%GeneratorOpCode{ + id: hashed_id, + name: datetime, + op: op + }) + + {:error, error} -> + {:error, error} + end + end + end + + @spec wait_for_event(Context.t(), id(), map()) :: map() + def wait_for_event(%{steps: steps} = _ctx, step_id, opts) do + op = %UnhashedOp{name: step_id, op: "WaitForEvent"} + hashed_id = UnhashedOp.hash(op) + + if steps |> Map.has_key?(hashed_id) do + case steps |> Map.get(hashed_id) do + nil -> nil + event -> Event.from(event) + end + else + opts = + opts + |> Enum.reduce(%{}, fn + {key, value}, acc -> Map.put(acc, key, value) + keyword, acc when is_list(keyword) -> Enum.into(keyword, acc) + end) + + opts = + cond do + Map.has_key?(opts, :match) -> + match = Map.get(opts, :match) + timeout = Map.get(opts, :timeout) + event = Map.get(opts, :event) + %{event: event, timeout: timeout, if: "event.#{match} == async.#{match}"} + + Map.has_key?(opts, :if) -> + Map.take(opts, [:event, :timeout, :if]) + + true -> + Map.take(opts, [:event, :timeout]) + end + + throw(%GeneratorOpCode{ + id: hashed_id, + name: step_id, + op: op.op, + opts: opts + }) + end + end + + def send_event(%{steps: steps} = _ctx, step_id, events) do + op = %UnhashedOp{name: step_id, op: "Step"} + hashed_id = UnhashedOp.hash(op) + + case Map.get(steps, hashed_id) do + nil -> + display_name = + if is_map(events) do + Map.get(events, :name, step_id) + else + step_id + end + + # if not, execute function + result = Inngest.Client.send(events) + + # cancel execution and return with opcode + throw(%GeneratorOpCode{ + id: hashed_id, + name: "sendEvent", + display_name: "Send " <> display_name, + op: op.op, + data: result + }) + + # if found, return value + val -> + val + end + end +end diff --git a/lib/inngest/trigger.ex b/lib/inngest/trigger.ex index e9d62f0..8c02813 100644 --- a/lib/inngest/trigger.ex +++ b/lib/inngest/trigger.ex @@ -1,4 +1,4 @@ -defmodule Inngest.Function.Trigger do +defmodule Inngest.Trigger do @moduledoc """ Struct representing a function trigger. @@ -7,10 +7,10 @@ defmodule Inngest.Function.Trigger do ## Examples # defining an event trigger - %Inngest.Function.Trigger{event: "auth/signup.email"} + %Inngest.Trigger{event: "auth/signup.email"} # defining a cron trigger, and can accept a timezone - %Inngest.Function.Trigger{cron: "TZ=America/Los_Angeles * * * * *"} + %Inngest.Trigger{cron: "TZ=America/Los_Angeles * * * * *"} """ defstruct [ @@ -29,7 +29,7 @@ defmodule Inngest.Function.Trigger do } end -defimpl Jason.Encoder, for: Inngest.Function.Trigger do +defimpl Jason.Encoder, for: Inngest.Trigger do def encode(%{cron: cron} = value, opts) when is_binary(cron) do Jason.Encode.map(Map.take(value, [:cron]), opts) end diff --git a/mix.exs b/mix.exs index 90e6174..f3f5f09 100644 --- a/mix.exs +++ b/mix.exs @@ -49,9 +49,9 @@ defmodule Inngest.MixProject do Function: [ Inngest.Event, Inngest.Function, - Inngest.Function.Trigger, + Inngest.Trigger, Inngest.Function.Step, - Inngest.Function.Handler + Inngest.Function.Input ], Router: [ Inngest.Router, diff --git a/test/inngest/function/handler_test.exs b/test/inngest/function/handler_test.exs index 38679a9..af6b997 100644 --- a/test/inngest/function/handler_test.exs +++ b/test/inngest/function/handler_test.exs @@ -1,479 +1,479 @@ -defmodule Inngest.Function.HandlerTest do - use ExUnit.Case, async: true - - alias Inngest.{Event, Enums, TestEventFn} - alias Inngest.Function.{Handler, GeneratorOpCode} - - describe "invoke/2" do - @run_id "01H4E9105QZNAZHGFRF14VCE2K" - @init_params %{ - "ctx" => %{ - "env" => "local", - "fn_id" => "dce563f1-ee74-4d68-b8b0-86de91c5a83f", - "run_id" => @run_id, - "stack" => %{ - "current" => 0, - "stack" => [] - } - }, - "steps" => %{} - } - @event %Event{name: "test event", data: %{"yo" => "lo"}} - - @step1_hash "BD01C51E32280A0F2A0C50EFDA6B47AB1A685ED9" - @step2_hash "AAB4F015B1D26D76C015B987F32E28E0869E7636" - @step3_hash "C3C14E4F5420C304AF2FDEE2683C4E31E15B3CC2" - @step4_hash "EC9FE031264AB8889294A32EC361BB9412ACDBD1" - @step5_hash "0B16CEB48DB1E67131278943647BAB213494B636" - - @sleep1_hash "145E2844A2497AB79D89CAFF7C8CCA0CC7F114AE" - @sleep2_hash "D924BC0E9DE36100A8DB3B932934FFE9357BBC46" - @sleep_until_hash "8FD581C437A99A584B0186168DB25F8D8AF7D6B5" - - setup do - %{ - handler: TestEventFn.__handler__(), - args: %{event: @event, run_id: @run_id, params: @init_params} - } - end - - test "initial invoke returns result of 1st step", %{handler: handler, args: args} do - assert {206, result} = Handler.invoke(handler, args) - opcode = Enums.opcode(:step_run) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @step1_hash, - name: "step1", - data: %{ - run: "something", - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - } - } - ] = result - end - - test "2nd invoke returns 2s sleep", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - } - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 1) - |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_sleep) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @sleep1_hash, - name: "2s", - data: nil - } - ] = result - end - - test "3rd invoke returns result of 2nd step", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @sleep1_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 2) - |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @sleep1_hash]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_run) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @step2_hash, - name: "step2", - data: %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - } - } - ] = result - end - - test "4th invoke returns another 3s sleep", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @sleep1_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 3) - |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @sleep1_hash, @step2_hash]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_sleep) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @sleep2_hash, - name: "3s", - data: nil - } - ] = result - end - - test "5th invoke returns sleep until", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @sleep1_hash => nil, - @sleep2_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 4) - |> put_in([:params, "ctx", "stack", "stack"], [ - @step1_hash, - @sleep1_hash, - @step2_hash, - @sleep2_hash - ]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_sleep) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @sleep_until_hash, - name: "2023-07-12T06:35:00Z", - data: nil - } - ] = result - end - - test "6th invoke returns result of 3rd step", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @sleep1_hash => nil, - @sleep2_hash => nil, - @sleep_until_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 5) - |> put_in([:params, "ctx", "stack", "stack"], [ - @step1_hash, - @sleep1_hash, - @step2_hash, - @sleep2_hash, - @sleep_until_hash - ]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_run) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @step3_hash, - name: "step3", - data: %{ - step: "final", - fn_count: 3, - step1_count: 1, - step2_count: 1, - run: "again" - } - } - ] = result - end - - test "7th invoke returns result of step 4", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @step3_hash => %{ - step: "final", - fn_count: 3, - step1_count: 1, - step2_count: 1, - run: "again" - }, - @sleep1_hash => nil, - @sleep2_hash => nil, - @sleep_until_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 6) - |> put_in([:params, "ctx", "stack", "stack"], [ - @step1_hash, - @sleep1_hash, - @step2_hash, - @sleep2_hash, - @sleep_until_hash, - @step3_hash - ]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_run) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @step4_hash, - name: "step4", - data: "foobar" - } - ] = result - end - - test "8th invoke returns result of step 5", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @step3_hash => %{ - step: "final", - fn_count: 3, - step1_count: 1, - step2_count: 1, - run: "again" - }, - @step4_hash => "foobar", - @sleep1_hash => nil, - @sleep2_hash => nil, - @sleep_until_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 7) - |> put_in([:params, "ctx", "stack", "stack"], [ - @step1_hash, - @sleep1_hash, - @step2_hash, - @sleep2_hash, - @sleep_until_hash, - @step3_hash, - @step4_hash - ]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_run) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @step5_hash, - name: "step5", - data: nil - } - ] = result - end - - test "9th invoke returns result of remaining run", %{handler: handler, args: args} do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @step3_hash => %{ - step: "final", - fn_count: 3, - step1_count: 1, - step2_count: 1, - run: "again" - }, - @step4_hash => "foobar", - @step5_hash => nil, - @sleep1_hash => nil, - @sleep2_hash => nil, - @sleep_until_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 8) - |> put_in([:params, "ctx", "stack", "stack"], [ - @step1_hash, - @sleep1_hash, - @step2_hash, - @sleep2_hash, - @sleep_until_hash, - @step3_hash, - @step4_hash, - @step5_hash - ]) - |> put_in([:params, "steps"], current_state) - - # Invoke - assert {200, result} = Handler.invoke(handler, args) - - assert %{ - "step4" => "foobar", - step: "final", - fn_count: 4, - step1_count: 1, - step2_count: 1, - run: "again", - yo: "lo" - } = result - end - - test "ignore step2 hash and execute sleep 2s due to stack out of order", %{ - handler: handler, - args: args - } do - # return data from step 1 - current_state = %{ - @step1_hash => %{ - step: "hello world", - fn_count: 1, - step1_count: 1, - step2_count: 0 - }, - @step2_hash => %{ - step: "yolo", - fn_count: 2, - step1_count: 1, - step2_count: 1 - }, - @sleep1_hash => nil - } - - args = - args - |> put_in([:params, "ctx", "stack", "current"], 3) - |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @step2_hash, @sleep1_hash]) - |> put_in([:params, "steps"], current_state) - - opcode = Enums.opcode(:step_sleep) - - # Invoke - assert {206, result} = Handler.invoke(handler, args) - - assert [ - %GeneratorOpCode{ - op: ^opcode, - id: @sleep1_hash, - name: "2s", - data: nil - } - ] = result - end - end -end +# defmodule Inngest.Function.HandlerTest do +# use ExUnit.Case, async: true + +# alias Inngest.{Event, Enums, TestEventFn} +# alias Inngest.Function.GeneratorOpCode + +# describe "invoke/2" do +# @run_id "01H4E9105QZNAZHGFRF14VCE2K" +# @init_params %{ +# "ctx" => %{ +# "env" => "local", +# "fn_id" => "dce563f1-ee74-4d68-b8b0-86de91c5a83f", +# "run_id" => @run_id, +# "stack" => %{ +# "current" => 0, +# "stack" => [] +# } +# }, +# "steps" => %{} +# } +# @event %Event{name: "test event", data: %{"yo" => "lo"}} + +# @step1_hash "BD01C51E32280A0F2A0C50EFDA6B47AB1A685ED9" +# @step2_hash "AAB4F015B1D26D76C015B987F32E28E0869E7636" +# @step3_hash "C3C14E4F5420C304AF2FDEE2683C4E31E15B3CC2" +# @step4_hash "EC9FE031264AB8889294A32EC361BB9412ACDBD1" +# @step5_hash "0B16CEB48DB1E67131278943647BAB213494B636" + +# @sleep1_hash "145E2844A2497AB79D89CAFF7C8CCA0CC7F114AE" +# @sleep2_hash "D924BC0E9DE36100A8DB3B932934FFE9357BBC46" +# @sleep_until_hash "8FD581C437A99A584B0186168DB25F8D8AF7D6B5" + +# setup do +# %{ +# handler: TestEventFn.__handler__(), +# args: %{event: @event, run_id: @run_id, params: @init_params} +# } +# end + +# test "initial invoke returns result of 1st step", %{handler: handler, args: args} do +# assert {206, result} = Handler.invoke(handler, args) +# opcode = Enums.opcode(:step_run) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @step1_hash, +# name: "step1", +# data: %{ +# run: "something", +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# } +# } +# ] = result +# end + +# test "2nd invoke returns 2s sleep", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# } +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 1) +# |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_sleep) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @sleep1_hash, +# name: "2s", +# data: nil +# } +# ] = result +# end + +# test "3rd invoke returns result of 2nd step", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @sleep1_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 2) +# |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @sleep1_hash]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_run) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @step2_hash, +# name: "step2", +# data: %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# } +# } +# ] = result +# end + +# test "4th invoke returns another 3s sleep", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @sleep1_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 3) +# |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @sleep1_hash, @step2_hash]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_sleep) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @sleep2_hash, +# name: "3s", +# data: nil +# } +# ] = result +# end + +# test "5th invoke returns sleep until", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @sleep1_hash => nil, +# @sleep2_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 4) +# |> put_in([:params, "ctx", "stack", "stack"], [ +# @step1_hash, +# @sleep1_hash, +# @step2_hash, +# @sleep2_hash +# ]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_sleep) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @sleep_until_hash, +# name: "2023-07-12T06:35:00Z", +# data: nil +# } +# ] = result +# end + +# test "6th invoke returns result of 3rd step", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @sleep1_hash => nil, +# @sleep2_hash => nil, +# @sleep_until_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 5) +# |> put_in([:params, "ctx", "stack", "stack"], [ +# @step1_hash, +# @sleep1_hash, +# @step2_hash, +# @sleep2_hash, +# @sleep_until_hash +# ]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_run) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @step3_hash, +# name: "step3", +# data: %{ +# step: "final", +# fn_count: 3, +# step1_count: 1, +# step2_count: 1, +# run: "again" +# } +# } +# ] = result +# end + +# test "7th invoke returns result of step 4", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @step3_hash => %{ +# step: "final", +# fn_count: 3, +# step1_count: 1, +# step2_count: 1, +# run: "again" +# }, +# @sleep1_hash => nil, +# @sleep2_hash => nil, +# @sleep_until_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 6) +# |> put_in([:params, "ctx", "stack", "stack"], [ +# @step1_hash, +# @sleep1_hash, +# @step2_hash, +# @sleep2_hash, +# @sleep_until_hash, +# @step3_hash +# ]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_run) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @step4_hash, +# name: "step4", +# data: "foobar" +# } +# ] = result +# end + +# test "8th invoke returns result of step 5", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @step3_hash => %{ +# step: "final", +# fn_count: 3, +# step1_count: 1, +# step2_count: 1, +# run: "again" +# }, +# @step4_hash => "foobar", +# @sleep1_hash => nil, +# @sleep2_hash => nil, +# @sleep_until_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 7) +# |> put_in([:params, "ctx", "stack", "stack"], [ +# @step1_hash, +# @sleep1_hash, +# @step2_hash, +# @sleep2_hash, +# @sleep_until_hash, +# @step3_hash, +# @step4_hash +# ]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_run) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @step5_hash, +# name: "step5", +# data: nil +# } +# ] = result +# end + +# test "9th invoke returns result of remaining run", %{handler: handler, args: args} do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @step3_hash => %{ +# step: "final", +# fn_count: 3, +# step1_count: 1, +# step2_count: 1, +# run: "again" +# }, +# @step4_hash => "foobar", +# @step5_hash => nil, +# @sleep1_hash => nil, +# @sleep2_hash => nil, +# @sleep_until_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 8) +# |> put_in([:params, "ctx", "stack", "stack"], [ +# @step1_hash, +# @sleep1_hash, +# @step2_hash, +# @sleep2_hash, +# @sleep_until_hash, +# @step3_hash, +# @step4_hash, +# @step5_hash +# ]) +# |> put_in([:params, "steps"], current_state) + +# # Invoke +# assert {200, result} = Handler.invoke(handler, args) + +# assert %{ +# "step4" => "foobar", +# step: "final", +# fn_count: 4, +# step1_count: 1, +# step2_count: 1, +# run: "again", +# yo: "lo" +# } = result +# end + +# test "ignore step2 hash and execute sleep 2s due to stack out of order", %{ +# handler: handler, +# args: args +# } do +# # return data from step 1 +# current_state = %{ +# @step1_hash => %{ +# step: "hello world", +# fn_count: 1, +# step1_count: 1, +# step2_count: 0 +# }, +# @step2_hash => %{ +# step: "yolo", +# fn_count: 2, +# step1_count: 1, +# step2_count: 1 +# }, +# @sleep1_hash => nil +# } + +# args = +# args +# |> put_in([:params, "ctx", "stack", "current"], 3) +# |> put_in([:params, "ctx", "stack", "stack"], [@step1_hash, @step2_hash, @sleep1_hash]) +# |> put_in([:params, "steps"], current_state) + +# opcode = Enums.opcode(:step_sleep) + +# # Invoke +# assert {206, result} = Handler.invoke(handler, args) + +# assert [ +# %GeneratorOpCode{ +# op: ^opcode, +# id: @sleep1_hash, +# name: "2s", +# data: nil +# } +# ] = result +# end +# end +# end diff --git a/test/inngest/function_test.exs b/test/inngest/function_test.exs index 8fcc33d..26a9b2a 100644 --- a/test/inngest/function_test.exs +++ b/test/inngest/function_test.exs @@ -2,12 +2,12 @@ defmodule Inngest.FunctionTest do use ExUnit.Case, async: true alias Inngest.Function - alias Inngest.Function.Trigger + alias Inngest.Trigger alias Inngest.{TestEventFn, TestCronFn} describe "slug/0" do test "return name of function as slug" do - assert "app-email-awesome-event-func" == TestEventFn.slug() + assert "test-event" == TestEventFn.slug() end end @@ -30,7 +30,7 @@ defmodule Inngest.FunctionTest do describe "serve/1" do test "event function should return approprivate map" do assert %{ - id: "app-email-awesome-event-func", + id: "test-event", name: "App / Email: Awesome Event Func", triggers: [ %Trigger{event: "my/awesome.event"} @@ -53,7 +53,7 @@ defmodule Inngest.FunctionTest do test "cron function should return appropriate map" do assert %{ - id: "awesome-cron-func", + id: "test-cron", name: "Awesome Cron Func", triggers: [ %Trigger{cron: "TZ=America/Los_Angeles * * * * *"} @@ -62,18 +62,6 @@ defmodule Inngest.FunctionTest do end end - describe "__handler__/0" do - test "returns the handler for the module including compiled steps" do - assert %Inngest.Function.Handler{ - mod: Inngest.TestEventFn, - file: _, - steps: steps - } = TestEventFn.__handler__() - - assert Enum.count(steps) == 11 - end - end - describe "validate_datetime/1" do @formats [ # RFC3339 diff --git a/test/inngest/router/helper_test.exs b/test/inngest/router/helper_test.exs index 350c086..28a6b3e 100644 --- a/test/inngest/router/helper_test.exs +++ b/test/inngest/router/helper_test.exs @@ -9,8 +9,8 @@ defmodule Inngest.Router.HelperTest do funcs = [Inngest.TestEventFn] assert %{ - "app-email-awesome-event-func" => %{ - id: "app-email-awesome-event-func", + "test-event" => %{ + id: "test-event", mod: Inngest.TestEventFn, steps: %{ step: %Inngest.Function.Step{ @@ -19,7 +19,7 @@ defmodule Inngest.Router.HelperTest do } }, triggers: [ - %Inngest.Function.Trigger{ + %Inngest.Trigger{ event: "my/awesome.event", expression: nil, cron: nil @@ -46,6 +46,7 @@ defmodule Inngest.Router.HelperTest do end end + @tag :skip test "should compile all modules in the provided paths" do expected = @dev_mods ++ @test_mods diff --git a/test/support/funcs.ex b/test/support/funcs.ex index 061e0e7..7c9d126 100644 --- a/test/support/funcs.ex +++ b/test/support/funcs.ex @@ -1,89 +1,103 @@ defmodule Inngest.TestEventFn do @moduledoc false - use Inngest.Function, - name: "App / Email: Awesome Event Func", - event: "my/awesome.event" - - @counts %{ - fn_count: 0, - step1_count: 0, - step2_count: 0 - } - - run "exec1" do - {:ok, %{run: "something"}} - end - - step "step1", %{data: data} do - result = - @counts - |> Map.merge(data) - |> Map.merge(%{ - step: "hello world", - fn_count: 1, - step1_count: 1 - }) - - {:ok, result} - end + use Inngest.Function + alias Inngest.{FnOpts, Trigger} - sleep "2s" - - step "step2", %{data: %{fn_count: fn_count, step1_count: step1_count}} do - {:ok, - %{ - step: "yolo", - fn_count: fn_count + 1, - step1_count: step1_count, - step2_count: 1 - }} - end + @func %FnOpts{id: "test-event", name: "App / Email: Awesome Event Func"} + @trigger %Trigger{event: "my/awesome.event"} - sleep "3s" - - run "exec2" do - {:ok, %{run: "again"}} - end - - sleep "until some date" do - "2023-07-12T06:35:00Z" - end + # @counts %{ + # fn_count: 0, + # step1_count: 0, + # step2_count: 0 + # } - step "step3", %{ - data: %{fn_count: fn_count, step1_count: step1_count, step2_count: step2_count, run: run} - } do - {:ok, - %{ - step: "final", - fn_count: fn_count + 1, - step1_count: step1_count, - step2_count: step2_count, - run: run - }} + @impl true + def exec(_ctx, _args) do + {:ok, "hello world"} end - step "step4" do - {:ok, "foobar"} - end - - step "step5" do - :ok - end - - run "final", %{data: %{fn_count: fn_count}} do - {:ok, - %{ - fn_count: fn_count + 1, - yo: "lo" - }} - end + # run "exec1" do + # {:ok, %{run: "something"}} + # end + + # step "step1", %{data: data} do + # result = + # @counts + # |> Map.merge(data) + # |> Map.merge(%{ + # step: "hello world", + # fn_count: 1, + # step1_count: 1 + # }) + + # {:ok, result} + # end + + # sleep("2s") + + # step "step2", %{data: %{fn_count: fn_count, step1_count: step1_count}} do + # {:ok, + # %{ + # step: "yolo", + # fn_count: fn_count + 1, + # step1_count: step1_count, + # step2_count: 1 + # }} + # end + + # sleep("3s") + + # run "exec2" do + # {:ok, %{run: "again"}} + # end + + # sleep "until some date" do + # "2023-07-12T06:35:00Z" + # end + + # step "step3", %{ + # data: %{fn_count: fn_count, step1_count: step1_count, step2_count: step2_count, run: run} + # } do + # {:ok, + # %{ + # step: "final", + # fn_count: fn_count + 1, + # step1_count: step1_count, + # step2_count: step2_count, + # run: run + # }} + # end + + # step "step4" do + # {:ok, "foobar"} + # end + + # step "step5" do + # :ok + # end + + # run "final", %{data: %{fn_count: fn_count}} do + # {:ok, + # %{ + # fn_count: fn_count + 1, + # yo: "lo" + # }} + # end end defmodule Inngest.TestCronFn do @moduledoc false - use Inngest.Function, - name: "Awesome Cron Func", - cron: "TZ=America/Los_Angeles * * * * *" + use Inngest.Function + alias Inngest.{FnOpts, Trigger} + + @func %FnOpts{id: "test-cron", name: "Awesome Cron Func"} + @trigger %Trigger{cron: "TZ=America/Los_Angeles * * * * *"} + + @impl true + def exec(_ctx, _args) do + {:ok, "cron"} + end end