Open Qqwy opened 4 years ago
I've been thinking about this a bit and discussing options with @wojtekmach. I've held off on implementing Conformable
for the other "primitive" types simply because I wanted to be conservative. I didn't really know if that was a good pattern or not, so I held off knowing we could implement it later. I think it probably does make sense to support binaries, integers, and ranges. It would definitely clean up some of the specs in our services.
Constants are a bit trickier. Its possible to implement something like a constant now, but its verbose and not very elegant: spec(& &1 == "some value")
. That's how we manage it today. But I think there is value in support constants as a first class entity.
Even with all of this, I don't think we have good support yet for your specific example. You could make it work, but again, its inelegant. The problem is that we don't have a good way to describe sequences of values. We can describe maps and generic collections. But we can't say, "this is a list where the first element should be 'users', the second element should be a UUID, and the third element should be 'posts'". I've been thinking for a bit that we need to support something like cat
which would allow you to concatenate a list of specs as a sequence. I think that's really the main piece that's missing here; we have most other constructs you'd need.
So with all of these ideas together, we could re-write your example like so:
def id_spec(), do: spec(is_string())
def posts_show_route(), do: cat([users: "users", user_id: id_spec(), posts: "posts", post_id: id_spec()])
def posts_route(), do: cat([users: "users", user_id: id_spec(), posts: "posts"])
def user_show_route(), do: cat([users: "users", user_id: id_spec()])
def users_route(), do: cat([users: "user"])
def routes(), do: one_of([posts_show_route(), posts_route(), users_show_route(), users_route()])
There's a lot of individual work to do here. But what do you think about these ideas?
Thank you for your response!
Its possible to implement something like a constant now, but its verbose and not very elegant:
spec(& &1 == "some value")
Yes. Besides being verbose/inelegant it will also not give you a usable property-checking generator. If you want that, the even more verbose
spec(&(&1 == val))
|> with_gen(StreamData.constant(val))
is currently required.
I think there is value in support constants as a first class entity.
:+1:
Even with all of this, I don't think we have good support yet for your specific example.
You are right. One idea would be to e.g. recognize the use of ++
inside the spec(...)
macro to concatenate together two lists or two other list-returning specs. Something like:
users_route(), do: spec(["users", id_spec()])
posts_route(), do: spec(users_route() ++ ["posts", id_spec()])
:thinking: ... on top of introducing ++/2
this would also require lists themselves being treated similarly to tuples by Norm
.
I think it might be a nice syntax, but it obviously is only one possibility in a large design space.
Currently, tuples and atoms already are converted to specs automatically (They implement the
Norm.Conformer.Conformable
protocol). I think it would make sense if that same protocol is implemented for other values that are often 'on their own':one_of
in a list of possible options.For other datatypes, I'd like to see a wrapper function
&constant/1
whose implementation is something likeOne question that remains is what should happen if we want to mix constants with other specs.
For instance, if I have a web-application with a 'posts' route nested under users, we end up with something like:
The problem in above example is passing a list immediately to
spec()
. One could write e.g.constant(["uses", id_spec()])
instead, but what would that mean?What do you think?