diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature index 5174e32c3e9..aeda06da754 100644 --- a/features/doctrine/date_filter.feature +++ b/features/doctrine/date_filter.feature @@ -4,7 +4,7 @@ Feature: Date filter on collections I need to retrieve collections filtered by date @createSchema - Scenario: Get collection filtered by date + Scenario: Get collection filtered by date ("after") Given there are 30 dummy objects with dummyDate When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28" Then the response status code should be 200 @@ -29,22 +29,44 @@ Feature: Date filter on collections {"pattern": "^/dummies/29$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 2, "maxItems": 2 }, + "hydra:totalItems": {"type":"number", "minimum": 2, "maximum": 2}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ + Scenario: Get collection filtered by date ("before") When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05" Then the response status code should be 200 And the response should be in JSON @@ -69,23 +91,47 @@ Feature: Date filter on collections {"pattern": "^/dummies/3$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 5, "maximum": 5}, "hydra:view": { "type": "object", "properties": { "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&page=1$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&page=2$"}, + "hydra:next": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&page=2$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:next" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28T00:00:00%2B00:00" + Scenario: Get collection filtered by date and time with timezone offset ("after") + When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28T01:00:00%2B01:00" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -108,23 +154,45 @@ Feature: Date filter on collections {"pattern": "^/dummies/29$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 2, "maxItems": 2 }, + "hydra:totalItems": {"type":"number", "minimum": 2, "maximum": 2}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28T00%3A00%3A00%2B00%3A00$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28T01%3A00%3A00%2B01%3A00&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28T01%3A00%3A00%2B01%3A00&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28T01%3A00%3A00%2B01%3A00&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05Z" + Scenario: Get collection filtered by date and time with UTC timezone ("before") + When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05T00:00:00Z" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -148,24 +216,46 @@ Feature: Date filter on collections {"pattern": "^/dummies/3$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 5, "maximum": 5}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05Z&page=1$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05T00%3A00%3A00Z&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05T00%3A00%3A00Z&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05T00%3A00%3A00Z&page=2$"}, + "hydra:next": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05T00%3A00%3A00Z&page=2$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:next" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Search for entities within a range - # The order should not influence the search + Scenario: Get collection filtered by date range ("before" followed by "after") When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05&dummyDate[after]=2015-04-05" Then the response status code should be 200 And the response should be in JSON @@ -188,22 +278,44 @@ Feature: Date filter on collections {"pattern": "^/dummies/5$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 1, "maxItems": 1 }, + "hydra:totalItems": {"type":"number", "minimum": 1, "maximum": 1}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&dummyDate%5Bafter%5D=2015-04-05$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&dummyDate%5Bafter%5D=2015-04-05&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&dummyDate%5Bafter%5D=2015-04-05&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&dummyDate%5Bafter%5D=2015-04-05&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ + Scenario: Get collection filtered by date range ("after" followed by "before") When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-05&dummyDate[before]=2015-04-05" Then the response status code should be 200 And the response should be in JSON @@ -226,23 +338,44 @@ Feature: Date filter on collections {"pattern": "^/dummies/5$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 1, "maxItems": 1 }, + "hydra:totalItems": {"type":"number", "minimum": 1, "maximum": 1}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-05&dummyDate%5Bbefore%5D=2015-04-05$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-05&dummyDate%5Bbefore%5D=2015-04-05&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-05&dummyDate%5Bbefore%5D=2015-04-05&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-05&dummyDate%5Bbefore%5D=2015-04-05&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Search for entities within an impossible range + Scenario: Get collection filtered by impossible date range When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-06&dummyDate[before]=2015-04-04" Then the response status code should be 200 And the response should be in JSON @@ -259,23 +392,42 @@ Feature: Date filter on collections "type": "array", "maxItems": 0 }, + "hydra:totalItems": {"type":"number", "minimum": 0, "maximum": 0}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-06&dummyDate%5Bbefore%5D=2015-04-04$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-06&dummyDate%5Bbefore%5D=2015-04-04&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-06&dummyDate%5Bbefore%5D=2015-04-04&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-06&dummyDate%5Bbefore%5D=2015-04-04&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Get collection filtered by association date + Scenario: Get collection filtered by date on a related entity ("after") Given there are 30 dummy objects with dummyDate and relatedDummy When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + Then print last JSON response And the JSON should be valid according to this schema: """ { @@ -296,23 +448,45 @@ Feature: Date filter on collections {"pattern": "^/dummies/60$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28&relatedDummy_dummyDate[after]=2015-04-28" + Scenario: Get collection filtered by date on a related entity ("after" with another potentially indistinguishable query parameter) + When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28&relatedDummy_dummyDate[after]=2015-04-27" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -336,23 +510,45 @@ Feature: Date filter on collections {"pattern": "^/dummies/60$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&relatedDummy_dummyDate%5Bafter%5D=2015-04-28$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&relatedDummy_dummyDate%5Bafter%5D=2015-04-27&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&relatedDummy_dummyDate%5Bafter%5D=2015-04-27&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&relatedDummy_dummyDate%5Bafter%5D=2015-04-27&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28T00:00:00%2B00:00" + Scenario: Get collection filtered by date and time with timezone offset on a related entity ("after") + When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-27T23:00:00-01:00" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -376,372 +572,88 @@ Feature: Date filter on collections {"pattern": "^/dummies/60$"} ] } - } + }, + "required": [ + "@id" + ] }, "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28T00%3A00%3A00%2B00%3A00$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } + "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-27T23%3A00%3A00-01%3A00&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-27T23%3A00%3A00-01%3A00&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-27T23%3A00%3A00-01%3A00&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ @createSchema - Scenario: Get collection filtered by association date + Scenario: Get collection filtered by date on a related entity but there are no matches ("after") Given there are 2 dummy objects with dummyDate and relatedDummy When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: + And the JSON should be valid according to this schema: """ { - "@context": "/contexts/Dummy", - "@id": "/dummies", - "@type": "hydra:Collection", - "hydra:member": [], - "hydra:totalItems": 0, - "hydra:view": { - "@id": "/dummies?relatedDummy.dummyDate%5Bafter%5D=2015-04-28", - "@type": "hydra:PartialCollectionView" + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/Dummy$"}, + "@id": {"pattern": "^/dummies$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "maxItems": 0 + }, + "hydra:totalItems": {"type":"number", "minimum": 0, "maximum": 0}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "hydra:search": { - "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}", - "hydra:variableRepresentation": "BasicRepresentation", - "hydra:mapping": [ - { - "@type": "IriTemplateMapping", - "variable": "dummyBoolean", - "property": "dummyBoolean", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.embeddedDummy.dummyBoolean", - "property": "relatedDummy.embeddedDummy.dummyBoolean", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[strictly_before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[strictly_after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[strictly_before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[after]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[strictly_after]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "description[exists]", - "property": "description", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[exists]", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyBoolean[exists]", - "property": "dummyBoolean", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy[exists]", - "property": "relatedDummy", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[id]", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[name]", - "property": "name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[description]", - "property": "description", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[relatedDummy.name]", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[relatedDummy.symfony]", - "property": "relatedDummy.symfony", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[dummyDate]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[between]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[gt]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[gte]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[lt]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[lte]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[between]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[gt]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[gte]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[lt]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[lte]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "id", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "id[]", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "name", - "property": "name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "alias", - "property": "alias", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "description", - "property": "description", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[]", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies[]", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummy", - "property": "dummy", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies.name", - "property": "relatedDummies.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.level", - "property": "relatedDummy.thirdLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.level[]", - "property": "relatedDummy.thirdLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.fourthLevel.level", - "property": "relatedDummy.thirdLevel.fourthLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.fourthLevel.level[]", - "property": "relatedDummy.thirdLevel.fourthLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.badFourthLevel.level", - "property": "relatedDummy.thirdLevel.badFourthLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.badFourthLevel.level[]", - "property": "relatedDummy.thirdLevel.badFourthLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", - "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[]", - "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "properties[]", - "property": null, - "required": false - } - ] - } + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ @@ -750,8 +662,8 @@ Feature: Date filter on collections Given there are 30 dummydate objects with dummyDate When I send a "GET" request to "/dummy_dates?dummyDate[after]=2015-04-28" Then the response status code should be 200 - And the JSON node "hydra:totalItems" should be equal to 3 And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "hydra:totalItems" should be equal to 3 @createSchema Scenario: Get collection filtered by date that is not a datetime including null after @@ -813,7 +725,7 @@ Feature: Date filter on collections @createSchema Scenario: Get collection filtered by embedded date Given there are 2 embedded dummy objects with dummyDate and embeddedDummy - When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyDate[after]=2015-04-28" + When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyDate[after]=2015-04-01" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -832,267 +744,43 @@ Feature: Date filter on collections "properties": { "@id": { "oneOf": [ - {"pattern": "^/embedded_dummies/28$"}, - {"pattern": "^/embedded_dummies/29$"} + {"pattern": "^/embedded_dummies/1$"}, + {"pattern": "^/embedded_dummies/2$"} ] } - } - }, - "hydra:search": { - "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyPrice,order[id],order[name],order[relatedDummy.symfony],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name}", - "hydra:variableRepresentation": "BasicRepresentation", - "hydra:mapping": [ - { - "@type": "IriTemplateMapping", - "variable": "dummyBoolean", - "property": "dummyBoolean", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[strictly_before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[strictly_after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[strictly_before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[after]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[strictly_after]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "description[exists]", - "property": "description", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[exists]", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy[exists]", - "property": "relatedDummy", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyBoolean[exists]", - "property": "dummyBoolean", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[id]", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[name]", - "property": "name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "order[relatedDummy.symfony]", - "property": "relatedDummy.symfony", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[between]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[gt]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[gte]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[lt]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyFloat[lte]", - "property": "dummyFloat", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[between]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[gt]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[gte]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[lt]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyPrice[lte]", - "property": "dummyPrice", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "id", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "id[]", - "property": "id", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "name", - "property": "name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "alias", - "property": "alias", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "description", - "property": "description", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[]", - "property": "relatedDummy.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies[]", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummy", - "property": "dummy", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies.name", - "property": "relatedDummies.name", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "properties[]", - "property": null, - "required": false - } - ] + }, + "required": [ + "@id" + ] }, - "hydra:view": { - "type": "object", - "properties": { - "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyDate%5Bafter%5D=2015-04-28$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - } + "minItems": 2, + "maxItems": 2 + }, + "hydra:totalItems": {"type":"number", "minimum": 2, "maximum": 2}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyDate%5Bafter%5D=2015-04-01&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyDate%5Bafter%5D=2015-04-01&page=1$"}, + "hydra:last": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyDate%5Bafter%5D=2015-04-01&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index b6f2e832131..0b9cdb1010e 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -17,14 +17,35 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 0}, "hydra:member": { "type": "array", "maxItems": 0 }, - "hydra:search": {} + "hydra:totalItems": {"type":"number", "minimum": 0, "maximum": 0}, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummies\\?page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems" + ] } """ @@ -42,7 +63,6 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "items": { @@ -55,10 +75,15 @@ Feature: Collections support {"pattern": "^/dummies/3$"} ] } - } + }, + "required": [ + "@id" + ] }, + "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30}, "hydra:view": { "type": "object", "properties": { @@ -67,9 +92,25 @@ Feature: Collections support "hydra:first": {"pattern": "^/dummies\\?page=1$"}, "hydra:last": {"pattern": "^/dummies\\?page=10$"}, "hydra:next": {"pattern": "^/dummies\\?page=2$"} - } + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:next" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ @@ -86,7 +127,6 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "items": { @@ -99,10 +139,15 @@ Feature: Collections support {"pattern": "^/dummies/21$"} ] } - } + }, + "required": [ + "@id" + ] }, + "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30}, "hydra:view": { "type": "object", "properties": { @@ -112,9 +157,26 @@ Feature: Collections support "hydra:last": {"pattern": "^/dummies\\?page=10$"}, "hydra:next": {"pattern": "^/dummies\\?page=8$"}, "hydra:previous": {"pattern": "^/dummies\\?page=6$"} - } + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:previous", + "hydra:next" + ], + "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ @@ -131,7 +193,6 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "items": { @@ -144,10 +205,15 @@ Feature: Collections support {"pattern": "^/dummies/30$"} ] } - } + }, + "required": [ + "@id" + ] }, + "minItems": 3, "maxItems": 3 }, + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30}, "hydra:view": { "type": "object", "properties": { @@ -156,16 +222,30 @@ Feature: Collections support "hydra:first": {"pattern": "^/dummies\\?page=1$"}, "hydra:last": {"pattern": "^/dummies\\?page=10$"}, "hydra:previous": {"pattern": "^/dummies\\?page=9$"} - } - }, - "hydra:search": {} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:previous" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ @!mongodb - Scenario: Enable the partial pagination client side + Scenario: Enable partial pagination from client side When I send a "GET" request to "/dummies?page=7&partial=1" Then the response status code should be 200 And the response should be in JSON @@ -178,7 +258,6 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "items": { @@ -191,8 +270,12 @@ Feature: Collections support {"pattern": "^/dummies/21$"} ] } - } + }, + "required": [ + "@id" + ] }, + "minItems": 3, "maxItems": 3 }, "hydra:view": { @@ -200,16 +283,29 @@ Feature: Collections support "properties": { "@id": {"pattern": "^/dummies\\?partial=1&page=7$"}, "@type": {"pattern": "^hydra:PartialCollectionView$"}, - "hydra:next": {"pattern": "^/dummies\\?partial=1&page=8$"}, - "hydra:previous": {"pattern": "^/dummies\\?partial=1&page=6$"} + "hydra:previous": {"pattern": "^/dummies\\?partial=1&page=6$"}, + "hydra:next": {"pattern": "^/dummies\\?partial=1&page=8$"} }, + "required": [ + "@id", + "@type", + "hydra:previous", + "hydra:next" + ], "additionalProperties": false } - } + }, + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:view" + ] } """ - Scenario: Disable the pagination client side + Scenario: Disable pagination from client side When I send a "GET" request to "/dummies?pagination=0" Then the response status code should be 200 And the response should be in JSON @@ -222,18 +318,24 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "minimum": 30}, "hydra:member": { "type": "array", - "minItems": 30 + "minItems": 30, + "maxItems": 30 }, - "hydra:search": {} + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30} }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems" + ] } """ - Scenario: Change the number of element by page client side + Scenario: Change number of items per page from client side When I send a "GET" request to "/dummies?page=2&itemsPerPage=10" Then the response status code should be 200 And the response should be in JSON @@ -246,12 +348,12 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "minItems": 10, "maxItems": 10 }, + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30}, "hydra:view": { "type": "object", "properties": { @@ -261,31 +363,30 @@ Feature: Collections support "hydra:last": {"pattern": "^/dummies\\?itemsPerPage=10&page=3$"}, "hydra:previous": {"pattern": "^/dummies\\?itemsPerPage=10&page=1$"}, "hydra:next": {"pattern": "^/dummies\\?itemsPerPage=10&page=3$"} - } - }, - "hydra:search": {} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last", + "hydra:previous", + "hydra:next" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Test presence of next - When I send a "GET" request to "/dummies?page=3" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be valid according to this schema: - """ - { - "@id":"/dummies?page=3", - "@type":"hydra:PartialCollectionView", - "hydra:first":"/dummies?page=1", - "hydra:last":"/dummies?page=10", - "hydra:previous":"/dummies?page=2", - "hydra:next":"/dummies?page=4" - } - """ - Scenario: Filter with exact match + Scenario: Retrieve a collection filtered by exact match (plain identifier) When I send a "GET" request to "/dummies?id=8" Then the response status code should be 200 And the response should be in JSON @@ -298,31 +399,50 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 1}, "hydra:member": { "type": "array", "items": { "type": "object", "properties": { "@id": {"pattern": "^/dummies/8$"} - } + }, + "required": [ + "@id" + ] }, + "minItems": 1, "maxItems": 1 }, + "hydra:totalItems": {"type":"number", "minimum": 1, "maximum": 1}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?id=8$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - }, - "hydra:search": {} + "@id": {"pattern": "^/dummies\\?id=8&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?id=8&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?id=8&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Filter with a raw URL + Scenario: Retrieve a collection filtered by exact match (IRI) When I send a "GET" request to "/dummies?id=%2fdummies%2f8" Then the response status code should be 200 And the response should be in JSON @@ -335,31 +455,50 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 1}, "hydra:member": { "type": "array", "items": { "type": "object", "properties": { "@id": {"pattern": "^/dummies/8$"} - } + }, + "required": [ + "@id" + ] }, + "minItems": 1, "maxItems": 1 }, + "hydra:totalItems": {"type":"number", "minimum": 1, "maximum": 1}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?id=%2Fdummies%2F8$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - }, - "hydra:search": {} + "@id": {"pattern": "^/dummies\\?id=%2Fdummies%2F8&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?id=%2Fdummies%2F8&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?id=%2Fdummies%2F8&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - Scenario: Filter with non-exact match + Scenario: Retrieve a collection filtered by partial match When I send a "GET" request to "/dummies?name=Dummy%20%238" Then the response status code should be 200 And the response should be in JSON @@ -372,32 +511,50 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 1}, "hydra:member": { "type": "array", "items": { "type": "object", "properties": { "@id": {"pattern": "^/dummies/8$"} - } + }, + "required": [ + "@id" + ] }, + "minItems": 1, "maxItems": 1 }, + "hydra:totalItems": {"type":"number", "minimum": 1, "maximum": 1}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?name=Dummy%20%238$"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - }, - "hydra:search": {} + "@id": {"pattern": "^/dummies\\?name=Dummy%20%238&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"}, + "hydra:first": {"pattern": "^/dummies\\?name=Dummy%20%238&page=1$"}, + "hydra:last": {"pattern": "^/dummies\\?name=Dummy%20%238&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ - @createSchema - Scenario: Allow passing 0 to `itemsPerPage` + Scenario: Change number of items per page to 0 from client side When I send a "GET" request to "/dummies?itemsPerPage=0" Then the response status code should be 200 And the response should be in JSON @@ -410,29 +567,41 @@ Feature: Collections support "@context": {"pattern": "^/contexts/Dummy$"}, "@id": {"pattern": "^/dummies$"}, "@type": {"pattern": "^hydra:Collection$"}, - "hydra:totalItems": {"type":"number", "maximum": 30}, "hydra:member": { "type": "array", "minItems": 0, "maxItems": 0 }, + "hydra:totalItems": {"type":"number", "minimum": 30, "maximum": 30}, "hydra:view": { "type": "object", "properties": { - "@id": {"pattern": "^/dummies\\?itemsPerPage=0$"}, + "@id": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"}, "@type": {"pattern": "^hydra:PartialCollectionView$"}, "hydra:first": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"}, - "hydra:last": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"}, - "hydra:previous": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"}, - "hydra:next": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"} - } - }, - "hydra:search": {} + "hydra:last": {"pattern": "^/dummies\\?itemsPerPage=0&page=1$"} + }, + "required": [ + "@id", + "@type", + "hydra:first", + "hydra:last" + ], + "additionalProperties": false + } }, - "additionalProperties": false + "required": [ + "@context", + "@id", + "@type", + "hydra:member", + "hydra:totalItems", + "hydra:view" + ] } """ + Scenario: Retrieve non-existent page when number of items per page is set to 0 from client side When I send a "GET" request to "/dummies?itemsPerPage=0&page=2" Then the response status code should be 400 And the JSON node "hydra:description" should be equal to "Page should not be greater than 1 if limit is equal to 0" diff --git a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php index 8b2492067a3..b4feeb63c56 100644 --- a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Common\Filter; use ApiPlatform\Core\Bridge\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Core\Exception\InvalidArgumentException; +use Psr\Log\LoggerInterface; /** * Trait for filtering the collection by date intervals. @@ -54,6 +56,8 @@ public function getDescription(string $resourceClass): array abstract protected function getProperties(): ?array; + abstract protected function getLogger(): LoggerInterface; + /** * Determines whether the given property refers to a date field. */ @@ -75,4 +79,17 @@ protected function getFilterDescription(string $property, string $period): array ], ]; } + + private function normalizeValue(string $value, string $dateTimeClass, string $property, string $parameterType): ?\DateTimeInterface + { + try { + return new $dateTimeClass($value); + } catch (\Exception $e) { + $this->getLogger()->notice('Invalid filter ignored', [ + 'exception' => new InvalidArgumentException(sprintf('Invalid value for "%s[%s]". Use date/time string supported by %s::__construct', $property, $parameterType, $dateTimeClass)), + ]); + + return null; + } + } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php index 275a153e369..6ae83acc4cc 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php @@ -69,7 +69,8 @@ protected function filterProperty(string $property, $values, Builder $aggregatio $matchField, self::PARAMETER_BEFORE, $values[self::PARAMETER_BEFORE], - $nullManagement + $nullManagement, + $property ); } @@ -79,7 +80,8 @@ protected function filterProperty(string $property, $values, Builder $aggregatio $matchField, self::PARAMETER_STRICTLY_BEFORE, $values[self::PARAMETER_STRICTLY_BEFORE], - $nullManagement + $nullManagement, + $property ); } @@ -89,7 +91,8 @@ protected function filterProperty(string $property, $values, Builder $aggregatio $matchField, self::PARAMETER_AFTER, $values[self::PARAMETER_AFTER], - $nullManagement + $nullManagement, + $property ); } @@ -99,7 +102,8 @@ protected function filterProperty(string $property, $values, Builder $aggregatio $matchField, self::PARAMETER_STRICTLY_AFTER, $values[self::PARAMETER_STRICTLY_AFTER], - $nullManagement + $nullManagement, + $property ); } } @@ -107,16 +111,9 @@ protected function filterProperty(string $property, $values, Builder $aggregatio /** * Adds the match stage according to the chosen null management. */ - private function addMatch(Builder $aggregationBuilder, string $field, string $operator, string $value, string $nullManagement = null): void + private function addMatch(Builder $aggregationBuilder, string $field, string $operator, string $value, ?string $nullManagement, string $property): void { - try { - $value = new \DateTime($value); - } catch (\Exception $e) { - // Silently ignore this filter if it can not be transformed to a \DateTime - $this->logger->notice('Invalid filter ignored', [ - 'exception' => new InvalidArgumentException(sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)), - ]); - + if (null === $value = $this->normalizeValue($value, 'DateTime', $property, $operator)) { return; } @@ -129,7 +126,7 @@ private function addMatch(Builder $aggregationBuilder, string $field, string $op if ((self::INCLUDE_NULL_BEFORE === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) || (self::INCLUDE_NULL_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true)) || - (self::INCLUDE_NULL_BEFORE_AND_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER, self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) + (self::INCLUDE_NULL_BEFORE_AND_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE, self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true)) ) { $aggregationBuilder->match()->addOr( $aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value), diff --git a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php index 0b619be243e..ce21ad70238 100644 --- a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php @@ -79,7 +79,8 @@ protected function filterProperty(string $property, $values, QueryBuilder $query self::PARAMETER_BEFORE, $values[self::PARAMETER_BEFORE], $nullManagement, - $type + $type, + $property ); } @@ -92,7 +93,8 @@ protected function filterProperty(string $property, $values, QueryBuilder $query self::PARAMETER_STRICTLY_BEFORE, $values[self::PARAMETER_STRICTLY_BEFORE], $nullManagement, - $type + $type, + $property ); } @@ -105,7 +107,8 @@ protected function filterProperty(string $property, $values, QueryBuilder $query self::PARAMETER_AFTER, $values[self::PARAMETER_AFTER], $nullManagement, - $type + $type, + $property ); } @@ -118,7 +121,8 @@ protected function filterProperty(string $property, $values, QueryBuilder $query self::PARAMETER_STRICTLY_AFTER, $values[self::PARAMETER_STRICTLY_AFTER], $nullManagement, - $type + $type, + $property ); } } @@ -128,20 +132,35 @@ protected function filterProperty(string $property, $values, QueryBuilder $query * * @param string|DBALType $type */ - protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null) + protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null/*, string $property*/) { + if (\func_num_args() > 8) { + $property = func_get_arg(8); + } else { + if (__CLASS__ !== \get_class($this)) { + $r = new \ReflectionMethod($this, __FUNCTION__); + if (__CLASS__ !== $r->getDeclaringClass()->getName()) { + @trigger_error(sprintf('Method %s() will have 9 non-optional arguments in API Platform 3.0. Not passing arguments 7-9 is deprecated since API Platform 2.4.', __FUNCTION__), E_USER_DEPRECATED); + } + } + } + $type = (string) $type; - try { - $value = false === strpos($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value); - } catch (\Exception $e) { - // Silently ignore this filter if it can not be transformed to a \DateTime - $this->logger->notice('Invalid filter ignored', [ - 'exception' => new InvalidArgumentException(sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)), - ]); + $dateTimeClass = false === strpos($type, '_immutable') ? 'DateTime' : 'DateTimeImmutable'; + if (null === $value = $this->normalizeValue($value, $dateTimeClass, $property ?? $field, $operator)) { return; } + /* + * Doctrine ORM/DBAL uses the PHP default timezone + * + * @see https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/working-with-datetime.html#default-timezone-gotcha + */ + if (false === strpos($type, 'tz')) { + $value = $value->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } + $valueParameter = $queryNameGenerator->generateParameterName($field); $operatorValue = [ self::PARAMETER_BEFORE => '<=', @@ -156,7 +175,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf } elseif ( (self::INCLUDE_NULL_BEFORE === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) || (self::INCLUDE_NULL_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true)) || - (self::INCLUDE_NULL_BEFORE_AND_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER, self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE], true)) + (self::INCLUDE_NULL_BEFORE_AND_AFTER === $nullManagement && \in_array($operator, [self::PARAMETER_BEFORE, self::PARAMETER_STRICTLY_BEFORE, self::PARAMETER_AFTER, self::PARAMETER_STRICTLY_AFTER], true)) ) { $queryBuilder->andWhere($queryBuilder->expr()->orX( $baseWhere, diff --git a/src/Hal/Serializer/CollectionNormalizer.php b/src/Hal/Serializer/CollectionNormalizer.php index d8266e9f406..6b716e1539f 100644 --- a/src/Hal/Serializer/CollectionNormalizer.php +++ b/src/Hal/Serializer/CollectionNormalizer.php @@ -32,7 +32,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer */ protected function getPaginationData($object, array $context = []): array { - [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); + [$isPaginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); $data = [ @@ -51,7 +51,7 @@ protected function getPaginationData($object, array $context = []): array $data['_links']['prev']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.); } - if ((null !== $lastPage && $currentPage !== $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) { + if (null !== $lastPage && $currentPage !== $lastPage || null === $lastPage && $pageTotalItems >= $itemsPerPage) { $data['_links']['next']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.); } } @@ -60,7 +60,7 @@ protected function getPaginationData($object, array $context = []): array $data['totalItems'] = $totalItems; } - if ($paginator) { + if ($isPaginator) { $data['itemsPerPage'] = (int) $itemsPerPage; } diff --git a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php index fc93165fa48..f28d2975c54 100644 --- a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php +++ b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php @@ -49,10 +49,14 @@ public function normalize($object, $format = null, array $context = []) return $data; } - $currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; + $currentPage = null; + $lastPage = null; + $itemsPerPage = null; + $pageTotalItems = null; if ($paginated = $object instanceof PartialPaginatorInterface) { if ($object instanceof PaginatorInterface) { - $paginated = 1. !== $lastPage = $object->getLastPage(); + $paginated = true; + $lastPage = $object->getLastPage(); } else { $itemsPerPage = $object->getItemsPerPage(); $pageTotalItems = (float) \count($object); diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php index c92b2548f13..d82a259bc27 100644 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ b/src/JsonApi/Serializer/CollectionNormalizer.php @@ -33,7 +33,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer */ protected function getPaginationData($object, array $context = []): array { - [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); + [$isPaginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); $data = [ @@ -61,7 +61,7 @@ protected function getPaginationData($object, array $context = []): array $data['meta']['totalItems'] = $totalItems; } - if ($paginator) { + if ($isPaginator) { $data['meta']['itemsPerPage'] = (int) $itemsPerPage; $data['meta']['currentPage'] = (int) $currentPage; } diff --git a/src/Serializer/AbstractCollectionNormalizer.php b/src/Serializer/AbstractCollectionNormalizer.php index bc9711a0119..5acb50ee1c3 100644 --- a/src/Serializer/AbstractCollectionNormalizer.php +++ b/src/Serializer/AbstractCollectionNormalizer.php @@ -116,13 +116,20 @@ protected function normalizeRawCollection($object, $format = null, array $contex */ protected function getPaginationConfig($object, array $context = []): array { - $currentPage = $lastPage = $itemsPerPage = $pageTotalItems = $totalItems = null; - $paginated = $paginator = false; + $currentPage = null; + $lastPage = null; + $itemsPerPage = null; + $pageTotalItems = null; + $totalItems = null; + $paginated = false; + $isPaginator = false; if ($object instanceof PartialPaginatorInterface) { - $paginated = $paginator = true; + $paginated = true; + $isPaginator = true; if ($object instanceof PaginatorInterface) { - $paginated = 1. !== $lastPage = $object->getLastPage(); + $paginated = true; + $lastPage = $object->getLastPage(); $totalItems = $object->getTotalItems(); } else { $pageTotalItems = (float) \count($object); @@ -134,7 +141,7 @@ protected function getPaginationConfig($object, array $context = []): array $totalItems = \count($object); } - return [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems]; + return [$isPaginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems]; } /**