chancancode / json_expressions

JSON matchmaking for all your API testing needs.
MIT License
415 stars 38 forks source link

Custom matcher which allows nil values #7

Closed tdumitrescu closed 11 years ago

tdumitrescu commented 11 years ago

We just started playing with json_expressions for testing API controllers at Lumos Labs, and it's awesome for using specs to document response formats concisely. One pattern we've needed repeatedly is a value which can be either nil or a specific type/set of values; for instance, a foreign key which can either be a Fixnum or nil. So I added a #nil_or matcher which always matches on nil, and otherwise delegates matching to the given pattern/value. So nil_or(Fixnum) will succeed with both 5 and nil, but not 'meow'. Thought it would be helpful for other people too.

chancancode commented 11 years ago

This is a great idea! Thanks for the pull request. I'll keep this open for now, sleep on it and thinking a little bit about the API in the next few days.

tdumitrescu commented 11 years ago

That's cool. If you can think of a clean way to get this behavior without changing the existing code, I'd be glad to hear it!

chancancode commented 11 years ago

Hey @tdumitrescu, just to let you know I haven't forgotten about this :) I just haven't made up my mind in terms of the APIs yet.

Re "If you can think of a clean way to get this behavior without changing the existing code" - json_expressions is written in a way to allow these kind of extension customization so what you did is perfectly valid and I think that's pretty much how I'd solve the same problem too.

Since this your code is here for everyone to see and it's simple enough for anyone to replicate that, I'm in no hurry to pull this in. I just want to give a little more thoughts on how the "shortcut"/helper methods should work/be called :)

tdumitrescu commented 11 years ago

Thanks for the update. Yeah, I'm not wedded to this particular solution, as long as the functionality's there. Another way I was thinking about is a method named something like #or_else so that you could write Fixnum.or_else(NilClass).or_else({bla: 'bla'}), which has the advantage of being more flexible and allowing chaining, but at the cost of invasiveness (monkeypatching Object). I'll be interested to see what you come up with!

chancancode commented 11 years ago

Lots of interesting ideas here. I haven't really been using json_expression the way you describe (make a pattern that's generic enough for all possible responses and that match that against each response), so it's interesting to think about how I could make it work better for that use case.

(In case you are wondering, I usually write specialized patterns specific each test case, so usually I know exactly what to expect in that particular case, hence I haven't been needing these kind of functionality for myself.)

On 2012-10-11, at 3:19 PM, tdumitrescu wrote:

Thanks for the update. Yeah, I'm not wedded to this particular solution, as long as the functionality's there. Another way I was thinking about is a method named something like #or_else so that you could write Fixnum.or_else(NilClass).or_else({bla: 'bla'}), which has the advantage of being more flexible and allowing chaining, but at the cost of invasiveness (monkeypatching Object). I'll be interested to see what you come up with!

— Reply to this email directly or view it on GitHub.

tdumitrescu commented 11 years ago

Interesting - the usage you describe (a specific pattern for each test case) doesn't seem to need any flexible matchers at all (e.g., why write 'Fixnum' when you're really testing for '5'?).

We've found that a powerful usage for our needs is when you specify a single pattern near the top of the spec, and then make sure that API actions in different test cases all produce responses which match it. That way, the spec file serves as explicit (and executable) documentation for the API's response format, which client authors can look at, rather than maintaining a separate set of docs outside the repo detailing the API format.

chancancode commented 11 years ago

I actually never needed to use all the Fixnum etc myself :P all i needed was unordered comparison for arrays and to ensure no unexpected keys in the json. all of those flexibility is more or less a by-product of using === instead of == for equality comparison. So in hindsight I am glad I made that decision, as that clearly enabled some use cases I didn't account for :)

I think your use case is pretty important, so I definitely want to provide the first class support it deserves. Since I'm currently not using the gem this way, by all means let me know if you run into other rough edges and we can figure out how to best deal with those.

One of those rough edges I imagine would be that you might want to capture a value and match it with a matcher at the same time.

Sent from my phone

On 2012-10-11, at 9:36 PM, tdumitrescu notifications@github.com wrote:

Interesting - the usage you describe (a specific pattern for each test case) doesn't seem to need any flexible matchers at all (e.g., why write 'Fixnum' when you're really testing for '5'?).

We've found that a powerful usage for our needs is when you specify a single pattern near the top of the spec, and then make sure that API actions in different test cases all produce responses which match it. That way, the spec file serves as explicit (and executable) documentation for the API's response format, which client authors can look at, rather than maintaining a separate set of docs outside the repo detailing the API format.

— Reply to this email directly or view it on GitHub.

martinstreicher commented 11 years ago

Not sure if this is related, but how do I express an array with zero or more elements of a certain set of elements? I cannot seem to find any tests or references to such an expression? So, warns: [] would be valid, or warns: [{message: 'Hello'}, {message: 'Goodbye'}] would also be valid.

chancancode commented 11 years ago

@martinstreicher Not sure if I understand you correctly - did you mean you want...

A) warns zero or more * arbitrary* elements

-or-

B) either warns contains exactly [{message: 'Hello'}, {message: 'Goodbye'}](nothing more, nothing less); or warn is []

If it's A, then {warns:[].ignore_extra_values!} should work.

If it's B, then you probably need to do something like { warns: ->(w){ ... } } and implement your own logic inside the Proc (for now)

chancancode commented 11 years ago

@tdumitrescu Sorry for the hold up - I haven't forgotten about this, just that I've been off API work for a while so I wasn't able to spend as much time on this. I am leaning towards a more generic implementation that allows arbitrary chaining, such as:

pattern = {
  a: m( [1, 2, 3] ).or( [] ).or( nil ),
  b: m( /\A[a-z]+\z/i ).and( /hello/i )
}

What do you think?

chancancode commented 11 years ago

Of course, there could be some optional syntactic sugar for these (probably off by default... overriding `` seems a little too crazy)

pattern = {
  a: `[1, 2, 3]` | `[]` | `nil`,
  b: `/\A[a-z]+\z/i` & `/hello/i`,
  c: `String` & !(`/hello/i`)
}

need to play around with this and see if there are any precedence issues though

tdumitrescu commented 11 years ago

i definitely like those chaining examples with an #or method - it's similar to the example i gave up above (Fixnum.or_else(NilClass).or_else({bla: 'bla'})) but by wrapping the first value in #m you get around the need to do any monkey-patching. as far as i can see, this would meet our needs very well.

i'm interested in seeing how you would implement the 'sugary' version...overriding one of ruby's basic operators to return a custom object? madness!

martinstreicher commented 11 years ago

I want

warn [{error: 'message'}*]

where * is the regex 0 or more of this specific element.

What does the proc receive?

On Jan 18, 2013, at 11:49 AM, Godfrey Chan wrote:

@martinstreicher Not sure if I understand you correctly - did you mean you want...

A) warns zero or more * arbitrary* elements

-or-

B) either warns contains exactly {message: 'Hello'}, {message: 'Goodbye'}; or warn is []

If it's A, then {warns:[].ignore_extra_values!} should work.

If it's B, then you probably need to do something like { warns: ->(w){ ... } } and implement your own logic inside the Proc (for now)

— Reply to this email directly or view it on GitHub.

chancancode commented 11 years ago

Hi Martin,

I don't have any super clever ways to do this yet, but this should work.

{ warn: ->(w){ w.all? { |e| ... } } }

The proc receives the object being matched.

On Friday, January 18, 2013, Martin Streicher wrote:

I want

warn [{error: 'message'}*]

where * is the regex 0 or more of this specific element.

What does the proc receive?

On Jan 18, 2013, at 11:49 AM, Godfrey Chan wrote:

@martinstreicher Not sure if I understand you correctly - did you mean you want...

A) warns zero or more * arbitrary* elements

-or-

B) either warns contains exactly {message: 'Hello'}, {message: 'Goodbye'}; or warn is []

If it's A, then {warns:[].ignore_extra_values!} should work.

If it's B, then you probably need to do something like { warns: ->(w){ ... } } and implement your own logic inside the Proc (for now)

— Reply to this email directly or view it on GitHub.

— Reply to this email directly or view it on GitHubhttps://github.com/chancancode/json_expressions/pull/7#issuecomment-12441659.

martinstreicher commented 11 years ago

OK -- will try it out. Perhaps what is needed is a .zero_or_more operator. I tried .forgiving!, but it seems best applied to non-empty collections.

On Jan 18, 2013, at 4:27 PM, Godfrey Chan wrote:

Hi Martin,

I don't have any super clever ways to do this yet, but this should work.

{ warn: ->(w){ w.all? { |e| ... } } }

The proc receives the object being matched.

On Friday, January 18, 2013, Martin Streicher wrote:

I want

warn [{error: 'message'}*]

where * is the regex 0 or more of this specific element.

What does the proc receive?

On Jan 18, 2013, at 11:49 AM, Godfrey Chan wrote:

@martinstreicher Not sure if I understand you correctly - did you mean you want...

A) warns zero or more * arbitrary* elements

-or-

B) either warns contains exactly {message: 'Hello'}, {message: 'Goodbye'}; or warn is []

If it's A, then {warns:[].ignore_extra_values!} should work.

If it's B, then you probably need to do something like { warns: ->(w){ ... } } and implement your own logic inside the Proc (for now)

— Reply to this email directly or view it on GitHub.

— Reply to this email directly or view it on GitHubhttps://github.com/chancancode/json_expressions/pull/7#issuecomment-12441659.

— Reply to this email directly or view it on GitHub.

martinstreicher commented 11 years ago

Yeah -- I did this bit of trickery..

warns: ->(w){ w.is_a?(Array) && (w.empty? || w.all? {|e| e.keys.one? && e.fetch('msg', nil).try(:is_a?, String)})} }

On Jan 18, 2013, at 4:27 PM, Godfrey Chan notifications@github.com wrote:

Hi Martin,

I don't have any super clever ways to do this yet, but this should work.

{ warn: ->(w){ w.all? { |e| ... } } }

The proc receives the object being matched.

On Friday, January 18, 2013, Martin Streicher wrote:

I want

warn [{error: 'message'}*]

where * is the regex 0 or more of this specific element.

What does the proc receive?

On Jan 18, 2013, at 11:49 AM, Godfrey Chan wrote:

@martinstreicher Not sure if I understand you correctly - did you mean you want...

A) warns zero or more * arbitrary* elements

-or-

B) either warns contains exactly {message: 'Hello'}, {message: 'Goodbye'}; or warn is []

If it's A, then {warns:[].ignore_extra_values!} should work.

If it's B, then you probably need to do something like { warns: ->(w){ ... } } and implement your own logic inside the Proc (for now)

— Reply to this email directly or view it on GitHub.

— Reply to this email directly or view it on GitHubhttps://github.com/chancancode/json_expressions/pull/7#issuecomment-12441659.

— Reply to this email directly or view it on GitHub.

chancancode commented 11 years ago

@tdumitrescu @martinstreicher While I (slowly) work on the next version I discovered json schema and a ruby implementation, which seems to be a wider effort on solving and standardizing some of the same problems here, what's your thoughts on those projects?

I am aware the goals are not 100% aligned but there's substantial overlap. I'm pondering how not to duplicate the effort and specialize in solving the unique problems here.

(I also found http://apiary.io/, which seems really cool.)

/cc @mathew-bb @pda @iangreenleaf

pda commented 11 years ago

I looked at json-schema… it looks interesting, but lacks the brevity of json_expressions. Perhaps json-schema could be a backend for json_expressions, providing correctness/robustness behind the nice brief API that json_expressions provides. I'd be lying if I said I had thought this idea through, though :)

I'm violently opposed to monkey-patching for Fixnum.or_else(NilClass).or_else({bla: 'bla'}), but a chainable wrapper as proposed somewhere up in the comments sounds nice. But it's probably just builder/sugar around a more plain API which also needs to be considered?

chancancode commented 11 years ago

Closed in favour of #18