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:
- Range filters for dates and values (e.g. remainingAttendeeCapacity>2 or startDate>2018-01-01T12:00:00)
- Component filters for dates, to only search on time component or date component (e.g. startDate>12:00)
2) The following for enum types and controlled vocabularies:
- Standard filtering using schema.org enums (e.g. genderRestriction=Male)
- Filtering using SKOS concepts (e.g. activity=Yoga)
- Restriction to set of enums (e.g. genderRestriction=Male or genderRestriction=Mixed)
3) The following regarding query structure:
- AND and OR nesting definition, to remove ambiguity
- Nested properties to be included in the filter, e.g. offers.price
4) Boolean values as primitive types, including null values (for undefined properties):
- isAccessibleForFree=true,null
5) Geo search
- Including a filter for radial and boundingBox 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/
- hydra:search is "deliberately fuzzy", so too generic to be applicable
- The common option of just using standard enum filters
?type=japanese,chinese&rating=4,5&days=sunday is not expressive enough
- The option of using OData style
?$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
- The option of using
{property_name}_from and {property_name}_to query range was also too simplistic, and not easily extensible.
- For filter functions on specific properties, creating objects such as
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".
- Standard filtering using schema.org enums (e.g. genderRestriction=Male)
?genderRestriction=Female (only the string following the # is required from e.g. ID https://www.openactive.io/ns#Female)
- Filtering using SKOS concepts (e.g. activity=Yoga)
?activity=d5f34cb1-35c0-46e5-ad6d-181f77274640 (only the string following the # is required from e.g. ID https://www.openactive.io/activity-list/#d5f34cb1-35c0-46e5-ad6d-181f77274640)
- Restriction to set of enums (e.g. genderRestriction=Male or genderRestriction=Mixed)
?genderRestriction=in:Female,Male (only the string following the # is required from e.g. ID https://www.openactive.io/ns#Female)
- Range filters for dates and values (e.g. remainingAttendeeCapacity>2 or startDate>2018-01-01T12:00:00)
?remainingAttendeeCapacity=gt:2
?startDate=gt:2018-01-01T12:00:00Z
?startDate=gt:2018-01-01T12:00:00Z&startDate=lt:2018-03-01T12:00:00Z
- Component filters for dates, to only search on time component or date component (e.g. startDate>12:00)
?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
- Boolean values as primitive types
?isAccessibleForFree=true
?isAccessibleForFree=in:true,null
- AND and OR nesting definition, to remove ambiguity
- AND represented by multiple params:
?startDate=gt:10:00Z&startDate=lt:14:00Z
- OR represented by specific operators:
?genderRestriction=in:Female,Male
- AND's connect each parameter, with ORs used within specific operator, hence
?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)
- Nested properties to be included in the filter, e.g. offers.price
- Use dot notation to access properties
?slot.startDate=gt:10:00Z&slot.startDate=lt:14:00Z
- Because operators are only available on numeric, date and enum types, and not strings, there is no issue of literal values matching operators.
- Boolean values as primitive types, including null values (for undefined properties):
- isAccessibleForFree=true,null
- Available values:
true, false, null
null is a reserved value for all types, and can be used to filter on where a specific property is undefined
- Including a filter of latitude, longitude, and radius
geo 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}
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=sundayis not expressive enough?$filter=price lt 10.00was 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$filtersyntax even for simple cases, where most APIs implement something similar to?status=open{property_name}_fromand{property_name}_toquery 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 ltee.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-01is 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,Maleis parsed as startDate>10:00 AND startDate<14:00 AND (genderRestriction = Female OR genderRestriction)?slot.startDate=gt:10:00Z&slot.startDate=lt:14:00Ztrue,false,nullnullis a reserved value for all types, and can be used to filter on where a specific property is undefinedgeoobjects 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}