Skip to content

Added support for QUERY method #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests-gs64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
name: GS64 v3.7.0 Unit Tests
services:
httpbin:
image: fortizpenaloza/httpbin
image: ghcr.io/ba-st-dependencies/httpbin:master
memcached:
image: memcached:1.6.7-alpine
steps:
Expand All @@ -27,7 +27,7 @@ jobs:
name: GS64 v3.7.1 Unit Tests
services:
httpbin:
image: fortizpenaloza/httpbin
image: ghcr.io/ba-st-dependencies/httpbin:master
memcached:
image: memcached:1.6.7-alpine
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
name: ${{ matrix.smalltalk }}
services:
httpbin:
image: fortizpenaloza/httpbin
image: ghcr.io/ba-st-dependencies/httpbin:master
ports:
- 127.0.0.1:80:80
memcached:
Expand Down
3 changes: 2 additions & 1 deletion .project
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
'srcDirectory' : 'source'
'srcDirectory' : 'source',
'tags': [ 'Buenos Aires Smalltalk' ]
}
8 changes: 8 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ lookup for details in the reference docs:
- [API Clients](reference/API-Client.md)
- [Service Discovery](reference/Service-Discovery.md)

To correctly run some of the tests from within the Pharo image,
you will need to run local containers of [httpbin](http://httpbin.org/) and
[memcached](https://memcached.org/), which can be achieved by executing:
`docker run -d -p 127.0.0.1:80:80 ghcr.io/ba-st-dependencies/httpbin:master`
and
`docker run -d -p 127.0.0.1:11211:11211 memcached:1.6.7-alpine`
respectively.

---

To use the project as a dependency of your project, take a look at:
Expand Down
23 changes: 17 additions & 6 deletions docs/reference/API-Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ API clients support the following methods:
- `deleteAt:configuredBy:withSuccessfulResponseDo:` will execute a DELETE against
the location in the first argument, configured by the second argument. If the
response is successful the last argument is evaluated with the response's body.
- `queryAt:configuredBy:withSuccessfulResponseDo:` will execute a QUERY against
the location in the first argument, configured by the second argument. If the
response is successful the last argument is evaluated with the response's body.

The configuration blocks follow the builder pattern defined [here](HTTP-Request.md).

Expand Down Expand Up @@ -71,6 +74,11 @@ It also supports some convenience methods built on the previous ones:
- `deleteAt:` will execute a `DELETE` against the location in the first argument.
If there's an entity tag associated with this location it will set the
`If-Match` header making the `DELETE` conditional.
- `query:at:accepting:withSuccessfulResponseDo:` will execute a `QUERY`,
whose body and `Content-Type` is defined by the entity in the first argument,
against the second argument, setting the `Accept` header according to the
third argument. If the response is successful the last argument is evaluated
with the response's body.

## Pooling

Expand Down Expand Up @@ -131,14 +139,17 @@ ExpiringCache onDistributedMemoryAt: serverList
where `serverList` is something like `{'127.0.0.1:11211'}`

Both kinds of caches take into account the `Cache-Control` headers received in
the responses. When the API client receives any of the `GET`-related messages it
looks up in the cache if there's a non-expired resource cached for this location.
the responses. When the API client receives any of the `GET` or
`QUERY`-related messages it looks up in the cache if there's a non-expired
resource cached for this location (also considering the content of the body
in the case of `QUERY`).

If it is, it will reuse that. In case there's no cached resource or the cached one
has expired it will proceed to execute the `GET`.
has expired it will proceed to execute the `GET` or `QUERY`.

Resources are cached when a successful `GET` response is received; and cleared when
any `POST`, `PUT`, `PATCH`, or `DELETE` method is executed against this location,
or when a cached resource is expired.
Resources are cached when a successful `GET` or `QUERY` response is received;
and cleared when any `POST`, `PUT`, `PATCH`, or `DELETE` method
is executed against this location, or when a cached resource is expired.

The following caching headers are supported:

Expand Down
1 change: 1 addition & 0 deletions docs/reference/HTTP-Request.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ later applied to an HTTP client (like `ZnClient` in Pharo).
- `delete:configuredUsing:`
- `patch:configuredUsing:`
- `put:configuredUsing:`
- `query:configuredUsing:`

where the first argument is anything convertible to an URL (it will receive the
`asUrl` message), and the second argument is a configuration closure providing
Expand Down
28 changes: 17 additions & 11 deletions source/Superluminal-Model/HttpRequest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,50 @@ Class {

{ #category : 'instance creation' }
HttpRequest class >> delete: aLocation configuredUsing: aBlock [
^ self performing: #DELETE on: aLocation configuredUsing: aBlock

^ self performing: #DELETE on: aLocation configuredUsing: aBlock
]

{ #category : 'instance creation' }
HttpRequest class >> get: aLocation [

^ self get: aLocation configuredUsing: [ ]
^ self get: aLocation configuredUsing: [ ]
]

{ #category : 'instance creation' }
HttpRequest class >> get: aLocation configuredUsing: aBlock [
^ self performing: #GET on: aLocation configuredUsing: aBlock

^ self performing: #GET on: aLocation configuredUsing: aBlock
]

{ #category : 'instance creation' }
HttpRequest class >> patch: aLocation configuredUsing: aBlock [

^ self performing: #PATCH on: aLocation configuredUsing: aBlock
^ self performing: #PATCH on: aLocation configuredUsing: aBlock
]

{ #category : 'private' }
HttpRequest class >> performing: anHttpMethod on: aLocation configuredUsing: aBlock [
^ self new initializePerforming: anHttpMethod on: aLocation asUrl configuredUsing: aBlock
HttpRequest class >> performing: anHttpMethod on: aLocation configuredUsing: aBlock [

^ self new initializePerforming: anHttpMethod on: aLocation asUrl configuredUsing: aBlock
]

{ #category : 'instance creation' }
HttpRequest class >> post: aLocation configuredUsing: aBlock [

^ self performing: #POST on: aLocation configuredUsing: aBlock
^ self performing: #POST on: aLocation configuredUsing: aBlock
]

{ #category : 'instance creation' }
HttpRequest class >> put: aLocation configuredUsing: aBlock [

^ self performing: #PUT on: aLocation configuredUsing: aBlock
^ self performing: #PUT on: aLocation configuredUsing: aBlock
]

{ #category : 'instance creation' }
HttpRequest class >> query: aLocation configuredUsing: aBlock [

^ self performing: #QUERY on: aLocation configuredUsing: aBlock
]

{ #category : 'applying' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ Class {
{ #category : 'test' }
ExpiringCacheKeyBuilderTest >> testEntity [

| httpRequest key |

httpRequest := HttpRequest
post: 'http://google.com'
configuredUsing: [ :request | request body contents: '[]' encodedAs: ZnMimeType applicationJson ].

key := ExpiringCacheKeyBuilder keyFor: httpRequest at: 'http://google.com'.
self assert: key equals: 'http://google.com/'
| httpRequest key |
httpRequest := HttpRequest
post: 'http://google.com'
configuredUsing: [ :request |
request body
contents: '{"name":"original"}'
encodedAs: ZnMimeType applicationJson ].

key := ExpiringCacheKeyBuilder keyFor: httpRequest at: 'http://google.com'.
self assert: key equals: 'http://google.com/|entity[{"name":"original"}]'
]

{ #category : 'test' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,97 @@ RESTfulAPIClientIntegrationTest >> testPutAtNotFound [
withMessageText: 'Cannot complete update'
]

{ #category : 'tests' }
RESTfulAPIClientIntegrationTest >> testQueryAtAcceptingWithSuccessfulResponseDo [

apiClient
query: ( ZnEntity json: '{"name":"original"}' )
at: self httpbinAnythingLocation
accepting: 'application/json;version=1.0.0' asMediaType
withSuccessfulResponseDo: [ :responseContents |
self withJsonFrom: responseContents do: [ :json |
self
assertUrl: json url equals: self httpbinAnythingLocation;
assert: json method equals: 'QUERY';
assert: ( json headers at: #Accept ) equals: 'application/json;version=1.0.0'.
self withJsonFrom: json data do: [ :data | self assert: data name equals: 'original' ]
]
]
]

{ #category : 'tests' }
RESTfulAPIClientIntegrationTest >> testQueryAtConfiguredByWithSuccessfulResponseDo [

apiClient
queryAt: self httpbinAnythingLocation
configuredBy: [ :request |
request queryString: [ :queryString | queryString fieldNamed: #step pairedTo: 20 ] ]
withSuccessfulResponseDo: [ :responseContents |
self withJsonFrom: responseContents do: [ :json |
self
assert: json data isEmpty;
assertUrl: json url equals: ( self httpbinAnythingLocation queryAt: 'step' put: 20 );
assert: json method equals: 'QUERY';
assert: ( json headers at: #Accept ) equals: '*/*'
]
]
]

{ #category : 'tests' }
RESTfulAPIClientIntegrationTest >> testQueryBadRequest [

self
should: [
apiClient
query: ( ZnEntity json: '["hi"]' )
at: self httpbinStatusLocation / '400'
accepting: 'application/json;version=1.0.0' asMediaType
withSuccessfulResponseDo: [ self fail ]
]
raise: HTTPClientError badRequest
withMessageText: 'Cannot complete the request'
]

{ #category : 'tests' }
RESTfulAPIClientIntegrationTest >> testQueryCached [

apiClient
query: ( ZnEntity json: '{"name":"original"}' )
at: self httpbinCacheLocation
accepting: 'application/json;version=1.0.0' asMediaType
withSuccessfulResponseDo: [ :responseContents |
self
withJsonFrom: responseContents
do: [ :json | self assertUrl: json url equals: self httpbinCacheLocation ]
].

apiClient
query: ( ZnEntity json: '{"name":"original"}' )
at: self httpbinCacheLocation
accepting: 'application/json;version=1.0.0' asMediaType
withSuccessfulResponseDo: [ :responseContents |
self withJsonFrom: responseContents do: [ :json |
self
assertUrl: json url equals: self httpbinCacheLocation;
assert: ( json headers at: #Accept ) equals: 'application/json;version=1.0.0'
]
]
]

{ #category : 'tests' }
RESTfulAPIClientIntegrationTest >> testQueryNotFound [

self
should: [
apiClient
query: ( ZnEntity json: '{"name":"original"}' )
at: self httpbinStatusLocation / '404'
accepting: 'application/json;version=1.0.0' asMediaType
withSuccessfulResponseDo: [ :responseContents | self fail ]
]
raise: HTTPClientError notFound
]

{ #category : 'private' }
RESTfulAPIClientIntegrationTest >> withJsonFrom: aString do: aBlock [

Expand Down
Loading
Loading