Skip to content
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

Feature request: specs for constant values #68

Open
Qqwy opened this issue Apr 11, 2020 · 2 comments
Open

Feature request: specs for constant values #68

Qqwy opened this issue Apr 11, 2020 · 2 comments

Comments

@Qqwy
Copy link
Contributor

Qqwy commented Apr 11, 2020

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':

  • Binaries. It is very common to use binary strings that follow certain patterns to e.g. parse user input. An example would be to match on routes/paths in a web-application. Some parts of these routes are dynamic, but most are static.
  • Integers. For better or worse, certain integer values are 'constants', expecially when used as part of a one_of in a list of possible options.
  • Possibly ranges, where the spec for a range will check if the input is inside the range. (and the generator will only generate integers inside the range).

For other datatypes, I'd like to see a wrapper function &constant/1 whose implementation is something like

def constant(val) do
  spec(&(&1 == val)) 
  |> with_gen(StreamData.constant(val))
end

One 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:

def id_spec(), do: spec(is_string())

def posts_show_route(), do: spec(["users", id_spec(), "posts", id_spec()])
def posts_route(), do: spec(["users", id_spec(), "posts"])
def user_show_route(), do: spec(["users", id_spec()])
def users_route(), do: spec(["users"])

def routes(), do: one_of(posts_show_route(), posts_route(), users_show_route(), users_route()]

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?

@keathley
Copy link
Member

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?

@Qqwy
Copy link
Contributor Author

Qqwy commented Apr 12, 2020

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.

👍


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()])

🤔 ... 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants