-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
maybe() and regex helper functions #30
Comments
I'm considering building something like |
Built-in support for regexs sounds very useful to me. Something Ive been thinking recently is similiar support for ranges. With these two we could:
and this is pretty appealing to me. (but Id still probably extract each spec into separate function) or maybe the solution is to use the Conformable protocol for these? |
I like the idea of using conformable on ranges! We could probably support generation that way as well. Regular expressions aren't a struct are they? Because that would also work in this scenario. Although generation would probably be a mess. |
Regexes are in fact structs, thats the beautiful part :D |
Perfect :) |
The benefit of a defmodule Membership do
# @behaviour NormPlugin
use Norm
def spec(members), do: Norm.spec(&(&1 in members))
def take(members, count) do
fn_reverse = fn {a, b} -> {b, a} end
fn_convert = &rem(&1, length(members))
lookup = members |> Enum.with_index() |> Enum.map(fn_reverse) |> Map.new()
spec = Norm.spec(is_integer() and (&(&1 > 0)))
spec |> gen() |> Enum.take(count) |> Enum.map(&Map.get(lookup, fn_convert.(&1)))
end
end Even without a proper plugin system, a directory of user-contributed "plugins", or rather helper modules, would be useful immediately. For example, to use the above as is: iex> days_of_week = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
iex> days_spec = Membership.spec(days_of_week)
#Norm.Spec<&(&1 in members)>
iex> days_of_week |> Enum.random() |> conform!(days_spec)
"Tue"
iex> days_of_week |> Membership.take(9)
["Mon", "Mon", "Wed", "Thu", "Fri", "Thu", "Mon", "Mon", "Sat"] |
I was playing around with a more natural behavior for a plugin: defmodule Membership do
# @behaviour NormPlugin
use Norm
def spec(members), do: Norm.spec(&(&1 in members))
def gen(members) do
fn_reverse = fn {a, b} -> {b, a} end
lookup = members |> Enum.with_index() |> Enum.map(fn_reverse) |> Map.new()
int_spec = Norm.spec(is_integer())
with_gen = with_gen(int_spec), StreamData.integer(1..length(members)))
Stream.map(Norm.gen(with_gen), &Map.get(lookup, &1))
end
end And then had the revelation that my Ideally, you'd want to define a And the bigger issue, and why perhaps this has to be integrated into Norm, is when generating data from (nested) schemas. I'll resist the urge to re-write the above as a GenServer that remembers its members as state... |
For your example I think we could get away with something like this defmodule Membership do
def spec(members) do
s = Norm.spec(& &1 in members)
g =
members
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
Norm.with_gen(s, g)
end
end
s = Membership.spec([1,2,3])
values = s |> Norm.gen()|> Enum.take(5)
for i <- values do
assert valid?(i, s)
end |
I'd have to think more about nested schemas. I think they could follow a similar pattern but I'd have to play around with it more. |
I like your code. But I'm not sure how to use An imperfect solution, if only because of the module population explosion: defmodule Membership do
defmacro __using__(_) do
quote do
use Norm
def s(), do: Norm.spec(&(&1 in __MODULE__.members()))
def gen() do
Norm.with_gen(
s(),
__MODULE__.members()
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
)
|> gen()
end
end
end
end
defmodule Days do
use Membership
def members(), do: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
end
defmodule Calendar do
use Norm
@contract favorite_day() :: Days.s()
def favorite_day(), do: "Fri"
end And with iex> Days.gen() |> Enum.take(5) |> conform(coll_of(Days.s()))
{:ok, ["Tue", "Thu", "Thu", "Tue", "Wed"]} |
And example code for a schema with the same behavior/interface: defmodule Todo do
use Norm
defstruct [:what, :when, :who]
def s(),
do:
schema(%{
what: spec(is_binary() and (&(String.length(&1) in 1..20))),
when: Days.s(),
who: coll_of(Team.s())
})
def gen(),
do:
Stream.repeatedly(fn ->
%__MODULE__{
what: Enum.random(["Wash dishes", "Grocery shop", "Watch movie", "Read book"]),
when: Days.gen() |> Enum.take(1) |> List.first(),
who: Team.gen() |> Enum.take(Enum.random(1..3)) |> MapSet.new()
}
end)
end
defmodule Membership do
defmacro __using__(_) do
quote do
use Norm
def s(), do: Norm.spec(&(&1 in __MODULE__.members()))
def gen(),
do:
Norm.with_gen(
s(),
__MODULE__.members()
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
)
|> Norm.gen()
end
end
end
defmodule Days do
use Membership
def members(), do: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
end
defmodule Team do
use Membership
def members(), do: ["Chris", "Stephen", "Wojtek"]
end In use: iex> Todo.gen() |> Enum.take(3) |> conform(coll_of(Todo.s()))
{:ok,
[
%Todo{what: "Wash dishes", when: "Thu", who: #MapSet<["Chris", "Stephen"]>},
%Todo{what: "Watch movie", when: "Sat", who: #MapSet<["Wojtek"]>},
%Todo{what: "Read book", when: "Fri", who: #MapSet<["Chris"]>}
]
} |
And for nested schemas: defmodule Todo do
use Norm
defstruct [:what, :when, :who]
def s(),
do:
schema(%{
what: spec(is_binary() and (&(String.length(&1) in 1..20))),
when: Days.s(),
who: coll_of(Person.s())
})
def gen(),
do:
Stream.repeatedly(fn ->
%__MODULE__{
what: Enum.random(["Wash dishes", "Grocery shop", "Watch movie", "Read book"]),
when: Days.gen() |> Enum.take(1) |> List.first(),
who: Person.gen() |> Enum.take(Enum.random(1..3)) |> MapSet.new()
}
end)
end
defmodule Person do
use Norm
defstruct [:name, :country]
def s(),
do:
schema(%{
name: spec(is_binary() and (&(String.length(&1) in 1..20))),
country: Country.s()
})
def gen(),
do:
Stream.repeatedly(fn ->
%__MODULE__{
name: Enum.random(["Chris", "Stephen", "Wojtek"]),
country: Country.gen() |> Enum.take(1) |> List.first()
}
end)
end
defmodule Membership do
defmacro __using__(_) do
quote do
use Norm
def s(), do: Norm.spec(&(&1 in __MODULE__.members()))
def gen(),
do:
Norm.with_gen(
s(),
__MODULE__.members()
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
)
|> Norm.gen()
end
end
end
defmodule Days do
use Membership
def members(), do: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
end
defmodule Country do
use Membership
def members(), do: ["Germany", "Italy", "Poland", "Philippines", "U.S.A."]
end In use: iex> Todo.gen() |> Enum.take(2)
[
%Todo{
what: "Wash dishes",
when: "Sat",
who: #MapSet<[
%Person{country: "France", name: "Wojtek"}
]>
},
%Todo{
what: "Grocery shop",
when: "Tue",
who: #MapSet<[
%Person{country: "Poland", name: "Wojtek"},
%Person{country: "U.S.A.", name: "Chris"}
]>
}
] |
And just to prove (to myself) that it (and Norm) works with algebraic data types with Algae: defmodule Todo do
use Norm
defstruct [:what, :when, :who]
def s(),
do:
schema(%{
what: spec(is_binary() and (&(String.length(&1) in 1..20))),
when: Days.s(),
who: coll_of(Person.s())
})
def gen(),
do:
Stream.repeatedly(fn ->
%__MODULE__{
what: Enum.random(["Wash dishes", "Grocery shop", "Watch movie", "Read book"]),
when: Days.gen() |> Enum.take(1) |> List.first(),
who: Person.gen() |> Enum.take(Enum.random(1..3)) |> MapSet.new()
}
end)
end
defmodule Person do
use Norm
import Algae
alias Algae.Maybe
defsum do
defdata Student do
name :: String.t()
school :: String.t()
end
defdata Programmer do
name :: String.t()
languages :: MapSet.t()
university :: Maybe.Just.t() | Maybe.Nothing.t()
end
end
def s(),
do:
schema(%{
name: spec(is_binary() and (&(String.length(&1) in 1..20))),
languages: coll_of(Language.s()),
school: School.s(),
university: spec(&(Maybe.from_maybe(&1, else: nil) in University.members()))
})
def gen(),
do:
Stream.repeatedly(fn ->
Enum.random([
%Person.Student{
name: Enum.random(["Sabrina", "Harvey", "Prudence"]),
school: School.gen() |> Enum.take(1) |> List.first()
},
%Person.Programmer{
name: Enum.random(["Chris", "Stephen", "Wojtek"]),
languages: Language.gen() |> Enum.take(Enum.random(1..3)) |> MapSet.new(),
university: University.gen() |> Enum.take(1) |> List.first() |> Maybe.from_nillable()
}
])
end)
end
defmodule Membership do
defmacro __using__(_) do
quote do
use Norm
def s(), do: Norm.spec(&(&1 in __MODULE__.members()))
def gen(),
do:
Norm.with_gen(
s(),
__MODULE__.members()
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
)
|> Norm.gen()
end
end
end
defmodule Days do
use Membership
def members(), do: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
end
defmodule Language do
use Membership
def members(), do: ["Elixir", "Haskell", "Cobol", "Elm"]
end
defmodule School do
use Membership
def members(), do: ["Academy of the Unseen Arts", "Baxter High School"]
end
defmodule University do
use Membership
def members(), do: [nil, "UMIST", "University of Southern California"]
end In use: iex> Todo.gen() |> Enum.take(2) |> IO.inspect() |> conform(coll_of(Todo.s()))
[
%Todo{
what: "Watch movie",
when: "Thu",
who: #MapSet<[
%Person.Programmer{
languages: #MapSet<["Elixir", "Elm"]>,
name: "Stephen",
university: %Algae.Maybe.Just{just: "UMIST"}
}
]>
},
%Todo{
what: "Watch movie",
when: "Sun",
who: #MapSet<[
%Person.Student{name: "Prudence", school: "Academy of the Unseen Arts"},
%Person.Programmer{
languages: #MapSet<["Elixir", "Elm", "Haskell"]>,
name: "Chris",
university: %Algae.Maybe.Nothing{}
}
]>
}
]
{:ok,
[
%Todo{
what: "Watch movie",
when: "Thu",
who: [
%Person.Programmer{
languages: ["Elixir", "Elm"],
name: "Stephen",
university: %Algae.Maybe.Just{just: "UMIST"}
}
]
},
%Todo{
what: "Watch movie",
when: "Sun",
who: [
%Person.Student{name: "Prudence", school: "Academy of the Unseen Arts"},
%Person.Programmer{
languages: ["Elixir", "Elm", "Haskell"],
name: "Chris",
university: %Algae.Maybe.Nothing{}
}
]
}
]} PS: It seems that |
In case they help others, here are a couple of "helper" functions that I've found useful.
maybe()
I often want specs or schemas that also accept a
nil
value. Rather than littering my specs withspec(is_nil() or (...))
, I have amaybe()
helper function:Example:
Perhaps it's something that could be included in the library as a bit of syntactic sugar.
regex()
When using
Regex.match?
in specs, it's important to also check foris_binary()
. Otherwise, when you send, say, anil
value you'll get ano function clause matching in Regex.match?/2
error with a stacktrace that only points to Norm's own code. (It will still crash when wrapped in mymaybe()
helper function.)So that I don't forget any
is_binary()
clauses, I use my ownregex()
helper function(s).Example:
Perhaps
match()
ormatch?()
would be better naming thanregex()
.The text was updated successfully, but these errors were encountered: