Open nickevansuk opened 6 years ago
@nickevansuk this looks interesting and very powerful
In a much simpler query parameter specification without a syntax like this, every single value would require a new separate query parameter e.g. startDate__gt=2018-...
and startDate__lt=2018-...
rather than startDate=lt:2018-...
and startDate=gt:2018-...
Cons to the much simpler approach:
Pros:
activity=gt:d5f34cb1-35c0-46e5-ad6d-181f77274640
. "Greater than" is a silly concept to apply to a UUID. In the simpler specification, activity__gt
simply wouldn't be definedMy main concern with the proposal is that it would be difficult to spec comprehensively with widely available tools like Swagger ("type": "string" doesn't really capture that the string can be "2", "gt:2", "in:2,3", etc). It would also be more effort to implement than something simpler and I suspect would leave developers or project managers spending time on deciding if "remainingAttendeeCapacity" needs the "in" operator or not 😝
To me, the qualities of a "simple" spec (simple to spec, simple to implement, simple to test) are:
startDate__gt=2018-07-17T13:00:00Z
or startDate__time__gt=13:00Z
rather than startDate
for either)Totally see where you're coming from @lukehesluke - in the interests of following at least one of the existing conventions, could that be expressed with square brackets instead (e.g startDate[gt]=2018-07-17T13:00:00Z
) as per https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/ ?
@nickevansuk sure I'm in favour of that
startDate=gt:2018-...&startDate=lt:2018-...
) as each operator will have its own key (e.g. startDate[gt]=2018-...&startDate[lt]=2018-...
)
We will use the natural query parameter logic of AND
-ing separate query parameters and OR
-ing repeated query parametersSome examples to further flesh this out:
startDate[lt]=2018-...
startDate[gte]=2018-...
startDate[time-lt]=21:30Z
startDate[time-gte]=18:00Z
ageRange.minValue[gte]=18
location.geo[radial]=51.5,-0.28,30
Actually should startDate
be subEvent.startDate
or subEvent[0].startDate
?
The square brackets does get in the way of indexing arrays by number
subEvent.0.startDate
subEvent.-1.startDate
This all looks excellent! And agree on #2, for simplicity of having one param (easier to reason about in terms of dev UX too)
One final thing we probably should check, how do different frameworks/languages interpret the square brackets? E.g. what does PHP do with it (i seem to recall it contructs foo[]=1&foo[]=2 into an array).
Assuming that there's no likely implementation issues, then lgtm
sorry just saw your other posts: good point, i guess we're assuming subEvent.startDate is just returning those subEvents that match the startDate (and those Events that contain such subEvents)?
Not sure if we'd need to subEvent[0].startDate, as the ordering of items in an array in the spec is currently not meaningful/important (except for offers, but that's only de-facto not in the spec)?
Could subEvent[].startDate
, or just assume it's never going to be a requirement and instead subEvent.startDate
Even with this logic it will be hard to represent "sort by junior price", as that's a property of one of the orders.price
s? That's probably niche enough in both implementation and requirement to have its own operator though?
@nickevansuk another remaining question is how we specify items in an array like for subEvent.startDate
subEvent[-1].startDate[gte]=2018-...
as "only include Events which have last occurrence later than time X" (assuming subEvents are ordered)subEvent[0].startDate[lt]=2018...
as "only include Events which have first occurrence greater than time X" (again - assuming subEvents are ordered)Ordering of items in subEvent is not important? I suppose I can see that for where subEvents can't strictly be sorted in time order (e.g. overlaps, multiple subEvents at the same time but with other differences)
Maybe a special operator for this use case like:
subEvent.startDate[latest-gte]=2018-...
subEvent.startDate[earliest-lt]=2018-...
As we're trying to reduce the amount of ambiguity / guesswork
See next comment for a better idea
Considering settling upon:
subEvent.startDate[gte]=2018-...
subEvent.startDate[lt]=2018-...
Where the explicit assumption is that, for arrays, the filter passes if any of the items in the array pass the filter
So in the first above case, this filter passes if ANY of the subEvent items have startDate greater than or equal to 2018-...
(some datetime)
@nickevansuk a conundrum:
startDate[time-gte]=23:30Z
During British Summer Time, 23:30Z is equivalent to 00:30 (BST). If the consumer of the opportunity API is a British app, making an API call during BST, they are likely filtering for sessions which, on any given day, start at or later than half past midnight local time (i.e. 00:30:00+01:00 -> 23:59:59+01:00). However, it is not clear to the API whether they mean 23:30:00Z -> 23:59:59Z or 23:30:00Z -> 22:59:59Z without knowing the consumer's understanding of local time
Given that this query parameter is going to be used to determine things like "get me all sessions in the morning" or "get me all sessions that start after I leave work", it would seem to me that this query parameter serves no value being timezone-aware and should instead expect a local time
This could be made a little more explicit by renaming the operator to:
startDate[local-time-gte]=00:30
One option is having the consumer always specify the end time
This can be enforced by putting the from and to in the same operator e.g.
startDate[time-range]=23:30Z,23:00Z
The range will now have to be cyclical in order to support non-UTC timezones. The above range would give 23.5 hours
You know what - this still doesn't work 😨
subEvent.startDate[time-range]=05:00Z,08:00Z
, Annie will return sessions that fall within the desired 6am to 9am range from the 25th - 27th. Curiously, the sessions from 28th - 1st will fall within an undesired 5am - 8am rangesubEvent.startDate[time-range]=06:00Z,09:00Z
, Annie will return the wrong sessions for the first two days (in BST) and then the right sessions for the remaining days (in GMT)
Charlie makes a query on sessions beginningThis problem is avoided only if time preferences are specified in local time.
This could either look like:
1.
subEvent.startDate[local-time-gte]=06:00
&subEvent.startDate[local-time-lte]=09:00
2.
subEvent.startDate[time-gte]=06:00,Europe/London
&subEvent.startDate[time-lte]=09:00,Europe/London
My personal preference is with option 1
Number 1 for me too, timezone names are ambiguous and so best to just remove that ambiguity
How does one know what local time is from an API call?
@nickevansuk when you say "one", I'm going to assume you're referring to the API consumer.
Local time is a property of a place. Any sessions located in Great Britain will have local time Europe/London
Sorry what I mean is subEvent.startDate[local-time-gte]=06:00
- what is 06:00? What is "local-time"? If feeds are presenting data in UTC (the current recommendation), how are we determining what 06:00 is in UTC for the query?
Sorry I now understand, subEvent.startDate[local-time-gte]=06:00 is always considering the startDate in the local time relative to that event. Hence if you searched for an event in the Japan it would return events that are 6pm in Japan, as if you book a holiday to Japan and you are searching for a Yoga class from London, you would still expect results to appear in Japan local time for when you land there.
TLDR;LGTM :)
Requirements
To allow for collections of resources to be filtered within the API, a standard approach to filtering/querying collections should be used.
The approach must be compatible with the PropertyValueSpecification for query string properties.
The approach should cover:
1) The following for numeric and date types:
2) The following for enum types and controlled vocabularies:
3) The following regarding query structure:
4) Boolean values as primitive types, including null values (for undefined properties):
5) Geo search
For example, for both of the endpoints below:
/sessions?
/facility-uses?
References
Discounted options
A good discussion of available options is available here: https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/
?type=japanese,chinese&rating=4,5&days=sunday
is not expressive enough?$filter=price lt 10.00
was overly expressive, and implied a greater complexity of query capability than is likely available in most cases. It also requires the use of a complex$filter
syntax even for simple cases, where most APIs implement something similar to?status=open
{property_name}_from
and{property_name}_to
query range was also too simplistic, and not easily extensible.location.geo.radialFilter.latitude={latitude}&location.geo.radial.longitude={longitude}&location.geo.radial.radius={radius}
intrudes on the properties namespace and extends the length of the GET request unnecessarily. Using the operator pattern consistently here resolves this e.g.location.geo=radial:{location.geo:radial.latitude},{location.geo:radial.longitude},{location.geo:radial.radius}
Proposal with Examples
Of all the options investigated, the OpenStack approach appears to be the most user-friendly, as it strikes the best balance between extensibility and familiarity.
The following prefixed operators are allowed:
in, nin, neq, gt, gte, lt, and lte
e.g.?size=gt:8
. A comma separated list as an operand is synonymous with "in
".?genderRestriction=Female
(only the string following the#
is required from e.g. IDhttps://www.openactive.io/ns#Female
)?activity=d5f34cb1-35c0-46e5-ad6d-181f77274640
(only the string following the#
is required from e.g. IDhttps://www.openactive.io/activity-list/#d5f34cb1-35c0-46e5-ad6d-181f77274640
)?genderRestriction=in:Female,Male
(only the string following the#
is required from e.g. IDhttps://www.openactive.io/ns#Female
)?remainingAttendeeCapacity=gt:2
?startDate=gt:2018-01-01T12:00:00Z
?startDate=gt:2018-01-01T12:00:00Z&startDate=lt:2018-03-01T12:00:00Z
?startDate=gt:10:00Z&startDate=lt:14:00Z
?startDate=gte:2018-01-01&startDate=lte:2018-01-01
is the same as?startDate=2018-01-01
- all will return results for startDate any time on 2018-01-01?isAccessibleForFree=true
?isAccessibleForFree=in:true,null
?startDate=gt:10:00Z&startDate=lt:14:00Z
?genderRestriction=in:Female,Male
?startDate=gt:10:00Z&startDate=lt:14:00Z&genderRestriction=in:Female,Male
is parsed as startDate>10:00 AND startDate<14:00 AND (genderRestriction = Female OR genderRestriction)?slot.startDate=gt:10:00Z&slot.startDate=lt:14:00Z
true
,false
,null
null
is a reserved value for all types, and can be used to filter on where a specific property is undefinedgeo
objects are afforded specific search objects:location.geo=radial:{location.geo:radial.latitude},{location.geo:radial.longitude},{location.geo:radial.radius}
location.geo=radial:{location.geo:radial.latitude},{location.geo:radial.longitude}
(automatic radius)location.geo=boundingBox:{location.geo:boundingBox.topLeft.latitude},{location.geo:boundingBox.topLeft.longitude},{location.geo:boundingBox.bottomRight.latitude},{location.geo:boundingBox.bottomRight.longitude}