Skip to content

Commit

Permalink
+ Timex.shift/2 wrappers
Browse files Browse the repository at this point in the history
+ function wrappers module with recursive_with_timeout wrapper
  • Loading branch information
xfynx committed Nov 18, 2019
1 parent fcb396f commit 659faef
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 3 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ by adding `ex_helpers` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_helpers, "~> 0.1.0"}
{:ex_helpers, "~> 0.2.0"}
]
end
```
Expand Down Expand Up @@ -69,8 +69,11 @@ Currently available modules:
- [ExHelpers.List](lib/ex_helpers/list.ex)
- [ExHelpers.Map](lib/ex_helpers/map.ex)
- [ExHelpers.Numeric](lib/ex_helpers/numeric.ex)
- [ExHelpers.Functions](lib/ex_helpers/functions.ex)

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/ex_helpers](https://hexdocs.pm/ex_helpers).

### TODO
typespecs for functions
3 changes: 3 additions & 0 deletions lib/ex_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ defmodule ExHelpers do
- [ExHelpers.List](ExHelpers.List.html)
- [ExHelpers.Map](ExHelpers.Map.html)
- [ExHelpers.Numeric](ExHelpers.Numeric.html)
- [ExHelpers.Functions](ExHelpers.Functions.html)
"""

use ExHelpers.Binary
use ExHelpers.DateTime
use ExHelpers.List
use ExHelpers.Map
use ExHelpers.Numeric
use ExHelpers.Functions

defmacro __using__(_opts) do
quote do
Expand All @@ -35,6 +37,7 @@ defmodule ExHelpers do
use ExHelpers.List
use ExHelpers.Map
use ExHelpers.Numeric
use ExHelpers.Functions
end
end
end
61 changes: 60 additions & 1 deletion lib/ex_helpers/date_time.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule ExHelpers.DateTime do
@moduledoc """
Currently module contain only one type of functions - convert any given binary to date.
Module contain functions to convert any (almost of it, for string patterns take a look at `to_date/1`)
date/datetime strings to date and wrappers arount `Timex.shift/2` for shorter shift datetime declaration.
For parsing to date used Timex as most famous date/datetime library.
**TODO: to_datetime functions**
Expand Down Expand Up @@ -76,10 +78,67 @@ defmodule ExHelpers.DateTime do
end
def to_date(_, _), do: nil

@doc """
take a look at `after_time/2`
"""
@spec after_time(integer) :: DateTime.t() | {:error, any}
def after_time(duration), do: after_time(duration, :seconds)
@doc """
Simple wrapper around `Timex.shift/2`.
Returns forward shift by `duration` in `granularity`.
- duration - integer value, > 0
- granularity - granularity metric, look at `Timex.Comparable.granularity/0`. `:seconds` by default
Examples:
```
after_time(5) # => 5 secs in future relatively Timex.now
after_time(5, :days) # => 5 days in future relatively Timex.now
```
"""
@spec after_time(integer, atom) :: DateTime.t() | {:error, any}
def after_time(duration, _) when duration < 1, do: {:error, :wrong_duration}
def after_time(duration, granularity), do: Timex.shift(Timex.now, [{granularity, duration}])

@doc """
take a look at `before_time/2`
"""
@spec before_time(integer) :: DateTime.t() | {:error, any}
def before_time(duration), do: before_time(duration, :seconds)
@doc """
Simple wrapper around `Timex.shift/2`.
Similar to `after_time/2`.
Returns backward shift by `duration` in `granularity`.
- duration - integer value, > 0
- granularity - granularity metric, look at `Timex.Comparable.granularity/0`. `:seconds` by default
Examples:
```
before_time(5) # => 5 secs in past relatively Timex.now
before_time(5, :days) # => 5 days in past relatively Timex.now
```
"""
@spec before_time(integer, atom) :: DateTime.t() | {:error, any}
def before_time(duration, _) when duration < 1, do: {:error, :wrong_duration}
def before_time(duration, granularity) do
case is_integer(duration) do
true -> Timex.shift(Timex.now, [{granularity, -duration}])
false -> {:error, {:invalid_shift, {granularity, duration}}}
end
end

defmacro __using__(_opts) do
quote do
defdelegate to_date(prop), to: ExHelpers.DateTime
defdelegate to_date(prop, patterns), to: ExHelpers.DateTime
defdelegate after_time(duration), to: ExHelpers.DateTime
defdelegate after_time(duration, granularity), to: ExHelpers.DateTime
defdelegate before_time(duration), to: ExHelpers.DateTime
defdelegate before_time(duration, granularity), to: ExHelpers.DateTime
end
end
end
72 changes: 72 additions & 0 deletions lib/ex_helpers/functions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule ExHelpers.Functions do
@moduledoc """
Module with function wrappers.
Currently available only `recursive_with_timeout/4`.
**TODO: async wrappers**
"""

@doc """
Recursive function call until result is given or timeout reached.
It is a wrapper function around executable anonymous function with timeout.
It's useful when you need to try execute some code or pass after some seconds of trying. For example,
if you need to fetch available worker in pool.
Simple example:
```
fun = fn->
# some logic inside
{:ok, __MODULE__}
end
time_to_exit = Timex.shift(Timex.now, seconds: 10)
# trying for 10 seconds after first call or if result is given in this timeout
recursive_with_timeout(fun, time_to_exit)
```
There is some limitation:
1. exit_time - variable of type `DateTime.t()`, when is reached without proper result, function stops.
2. fn_to_call - anonymous function (closure) with code to execute.
Should return tuple `{:ok, <anything>}` for proper exit or `{:error, <anything>}` for next call in wrapper.
If return spec is not like one of this, wrapper will return `{:error, :wrong_function_spec, <result of first call>}`
without further recursive calls.
You can customize timeout return message with argument `error_msg` - it will give you `{:error, <error_msg>}`
if timeout is reached. By default `error_msg=:timeout_reached`
You can customize timeout between recursive calls of closure with argument `sleep_before_call_in_ms`,
by default it's 100 milliseconds.
"""
@spec recursive_with_timeout(fun, integer, any) :: {:ok, any} | {:error, any} | {:error, any, any}
def recursive_with_timeout(fn_to_call, exit_time, error_msg \\ :timeout_reached, sleep_before_call_in_ms \\ 100) do
case Timex.compare(Timex.now, exit_time) do
# when exit_time isn't reached yet - call function (again or first time)
(-1) ->
case fn_to_call.() do
# tuple like {:ok, _} is good return value - return it in result.
{:ok, result} -> {:ok, result}
# tuple {:error, _} means that we need to call it again after sleep_before_call_in_ms
{:error, _} ->
:timer.sleep(sleep_before_call_in_ms)
recursive_with_timeout(fn_to_call, exit_time, error_msg)
# if return value is none of tuples above - it is wrong specification and we don't need to call it again, just
# return what we get from closure.
res -> {:error, :wrong_function_spec, res}
end
# when exit_time is reached, we don't need to call closure
_gt_or_eq -> {:error, error_msg}
end
end

defmacro __using__(_opts) do
quote do
defdelegate recursive_with_timeout(fn_to_call, exit_time),
to: ExHelpers.Functions
defdelegate recursive_with_timeout(fn_to_call, exit_time, error_msg),
to: ExHelpers.Functions
defdelegate recursive_with_timeout(fn_to_call, exit_time, error_msg, sleep_before_call_in_ms),
to: ExHelpers.Functions
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExHelpers.MixProject do
def project do
[
app: :ex_helpers,
version: "0.1.2",
version: "0.2.0",
elixir: ">= 1.3.0",
start_permanent: Mix.env() == :prod,
deps: deps()
Expand Down
25 changes: 25 additions & 0 deletions test/ex_helpers/date_time_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,29 @@ defmodule DateTimeTest do
assert to_date(~D[2018-01-01]) == ~D[2018-01-01]
end
end

describe "#after_time" do
test "should return date later then now by duration" do
assert Timex.shift(Timex.now, seconds: 5) <= after_time(5)
assert Timex.shift(Timex.now, days: 5) <= after_time(5, :days)
end
test "should return error if duration or granularity is wrong to use" do
assert after_time(0) == {:error, :wrong_duration}
assert after_time(-1) == {:error, :wrong_duration}
assert after_time("") == {:error, {:invalid_shift, {:seconds, ""}}}
assert after_time(1, :ddd) == {:error, {:invalid_shift, {:ddd, 1}}}
end
end
describe "#before_time" do
test "should return date sooner then now by duration" do
assert Timex.shift(Timex.now, seconds: -5) <= before_time(5)
assert Timex.shift(Timex.now, days: -5) <= before_time(5, :days)
end
test "should return error if duration or granularity is wrong to use" do
assert before_time(0) == {:error, :wrong_duration}
assert before_time(-1) == {:error, :wrong_duration}
assert before_time("") == {:error, {:invalid_shift, {:seconds, ""}}}
assert before_time(1, :ddd) == {:error, {:invalid_shift, {:ddd, -1}}}
end
end
end
49 changes: 49 additions & 0 deletions test/ex_helpers/functions_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule FunctionsTest do
use ExUnit.Case
doctest ExHelpers.Functions
alias ExHelpers.Functions
alias ExHelpers.DateTime
use Functions

describe "#recursive_with_timeout" do
test "should return closure result" do
fun = fn-> {:ok, __MODULE__} end
before_exec = Timex.now
seconds_to_exec = 10
assert recursive_with_timeout(fun, DateTime.after_time(seconds_to_exec)) == {:ok, __MODULE__}
after_exec = Timex.now
assert Timex.diff(before_exec, after_exec, :seconds) <= seconds_to_exec
end

test "should stop after exit time is reached" do
fun = fn-> {:error, "nomatter"} end
before_exec = Timex.now
seconds_to_exec = 3
upto = DateTime.after_time(seconds_to_exec)
assert recursive_with_timeout(fun, upto) == {:error, :timeout_reached}
after_exec = Timex.now
assert Timex.diff(before_exec, after_exec, :seconds) <= seconds_to_exec
assert recursive_with_timeout(fun, upto, "custom_error") == {:error, "custom_error"}
end

test "should return timeout_reached for exit_time in past" do
assert recursive_with_timeout(
fn-> {:error, 1} end,
Timex.parse("2010-10-01", "{YYYY}-{0M}-{D}")
) == {:error, :timeout_reached}
end

test "should return specification error if closure return value isn't matches by pattern" do
fun = fn-> "wrong return pattern" end
before_exec = Timex.now
seconds_to_exec = 10
assert recursive_with_timeout(fun, DateTime.after_time(seconds_to_exec)) == {
:error,
:wrong_function_spec,
"wrong return pattern"
}
after_exec = Timex.now
assert Timex.diff(before_exec, after_exec, :seconds) <= seconds_to_exec
end
end
end

0 comments on commit 659faef

Please sign in to comment.