From 0d6dfa18fe8b3380391b1afba413f4994c9b8989 Mon Sep 17 00:00:00 2001 From: Gabriel Omar Cotelli Date: Fri, 14 Jun 2024 19:18:00 -0300 Subject: [PATCH 1/2] Refactor hypermedia support to allow library users to use a different encoding strategy for hypermedia links Avoid configuring hypermedia related mappings for non-hypermedia driven handlers --- ...rmediaDrivenRESTfulRequestHandler.class.st | 29 ++++ .../RESTfulRequestHandler.class.st | 12 ++ .../RESTfulRequestHandlerBehavior.class.st | 12 ++ .../RESTfulRequestHandlerBuilder.class.st | 148 +++++++++--------- 4 files changed, 124 insertions(+), 77 deletions(-) diff --git a/source/Stargate-Model/HypermediaDrivenRESTfulRequestHandler.class.st b/source/Stargate-Model/HypermediaDrivenRESTfulRequestHandler.class.st index da20fb0..929e616 100644 --- a/source/Stargate-Model/HypermediaDrivenRESTfulRequestHandler.class.st +++ b/source/Stargate-Model/HypermediaDrivenRESTfulRequestHandler.class.st @@ -12,6 +12,35 @@ Class { #tag : 'HATEOAS' } +{ #category : 'encoding' } +HypermediaDrivenRESTfulRequestHandler class >> encode: resource toJsonUsing: writer as: schema within: requestContext [ + + writer + for: ResourceCollection do: [ :mapping | + ( mapping mapInstVar: #items ) valueSchema: #ResourceCollectionItems. + mapping mapAsHypermediaControls: [ :collection | + requestContext hypermediaControlsFor: collection items ] + ]; + for: #ResourceCollectionItems customDo: [ :mapping | mapping listOfElementSchema: schema ]. + + [ writer nextPut: resource as: schema ] + unless: ( resource isA: ResourceCollection ) + inWhichCase: [ writer nextPut: resource ] +] + +{ #category : 'encoding' } +HypermediaDrivenRESTfulRequestHandler class >> encode: resource toJsonUsing: writer within: requestContext [ + + writer for: ResourceCollection do: [ :mapping | + mapping + mapInstVar: #items; + mapAsHypermediaControls: [ :collection | + requestContext hypermediaControlsFor: collection items ] + ]. + + writer nextPut: resource +] + { #category : 'instance creation' } HypermediaDrivenRESTfulRequestHandler class >> resourceLocator: aResouceLocator paginationPolicy: aPaginationPolicy decodingRules: theDecodingRules encodingRules: theEncodingRules calculateEntityTagsWith: entityTagCalculator cachingDirectives: theCachingDirectives allowedLanguageTags: allowedLanguageTags drivenBy: aMediaControlsFactory handleErrorsWith: anExceptionHandler [ diff --git a/source/Stargate-Model/RESTfulRequestHandler.class.st b/source/Stargate-Model/RESTfulRequestHandler.class.st index f474294..84d71af 100644 --- a/source/Stargate-Model/RESTfulRequestHandler.class.st +++ b/source/Stargate-Model/RESTfulRequestHandler.class.st @@ -10,6 +10,18 @@ Class { #tag : 'Controllers' } +{ #category : 'encoding' } +RESTfulRequestHandler class >> encode: resource toJsonUsing: writer as: schema within: requestContext [ + + writer nextPut: resource as: schema +] + +{ #category : 'encoding' } +RESTfulRequestHandler class >> encode: resource toJsonUsing: writer within: requestContext [ + + writer nextPut: resource +] + { #category : 'instance creation' } RESTfulRequestHandler class >> resourceLocator: aResouceLocator paginationPolicy: aPaginationPolicy decodingRules: theDecodingRules encodingRules: theEncodingRules calculateEntityTagsWith: entityTagCalculator cachingDirectives: theCachingDirectives allowedLanguageTags: allowedLanguageTags handleErrorsWith: anExceptionHandler [ diff --git a/source/Stargate-Model/RESTfulRequestHandlerBehavior.class.st b/source/Stargate-Model/RESTfulRequestHandlerBehavior.class.st index fd5a4c5..93bf63f 100644 --- a/source/Stargate-Model/RESTfulRequestHandlerBehavior.class.st +++ b/source/Stargate-Model/RESTfulRequestHandlerBehavior.class.st @@ -19,6 +19,18 @@ Class { #tag : 'Controllers' } +{ #category : 'encoding' } +RESTfulRequestHandlerBehavior class >> encode: resource toJsonUsing: writer as: schema within: requestContext [ + + self subclassResponsibility +] + +{ #category : 'encoding' } +RESTfulRequestHandlerBehavior class >> encode: resource toJsonUsing: writer within: requestContext [ + + self subclassResponsibility +] + { #category : 'private' } RESTfulRequestHandlerBehavior >> applyCachingDirectivesFor: aResource to: response within: requestContext [ diff --git a/source/Stargate-Model/RESTfulRequestHandlerBuilder.class.st b/source/Stargate-Model/RESTfulRequestHandlerBuilder.class.st index a77b3ea..23222e5 100644 --- a/source/Stargate-Model/RESTfulRequestHandlerBuilder.class.st +++ b/source/Stargate-Model/RESTfulRequestHandlerBuilder.class.st @@ -9,11 +9,12 @@ Class { 'encodingRules', 'paginationPolicyFactory', 'entityTagFactoryBinding', - 'handlerFactory', 'resourceLocator', 'exceptionHandler', 'cachingDirectives', - 'allowedLanguageTags' + 'allowedLanguageTags', + 'requestHandlerClass', + 'requestHandlerFactory' ], #category : 'Stargate-Model-Controllers', #package : 'Stargate-Model', @@ -35,34 +36,37 @@ RESTfulRequestHandlerBuilder >> beHypermediaDriven [ { #category : 'configuring' } RESTfulRequestHandlerBuilder >> beHypermediaDrivenBy: aMediaControlsFactoryBlock [ - AssertionChecker - enforce: [ resourceLocator canLookupResources ] - because: 'Missing location resolution.'. - handlerFactory := [ HypermediaDrivenRESTfulRequestHandler - resourceLocator: resourceLocator - paginationPolicy: paginationPolicyFactory - decodingRules: decodingRules - encodingRules: encodingRules - calculateEntityTagsWith: entityTagFactoryBinding content - cachingDirectives: cachingDirectives - allowedLanguageTags: allowedLanguageTags - drivenBy: [ :builder :resource :requestContext :resourceLocation | - builder addAsSelfLink: resourceLocation. - aMediaControlsFactoryBlock - cull: builder - cull: resource - cull: requestContext - cull: resourceLocation. - builder build - ] - handleErrorsWith: exceptionHandler - ] + AssertionChecker + enforce: [ resourceLocator canLookupResources ] because: 'Missing location resolution.'; + enforce: [ encodingRules isEmpty ] + because: 'Encoding rules needs to be configured after hypermedia'. + requestHandlerClass := HypermediaDrivenRESTfulRequestHandler. + requestHandlerFactory := [ + requestHandlerClass + resourceLocator: resourceLocator + paginationPolicy: paginationPolicyFactory + decodingRules: decodingRules + encodingRules: encodingRules + calculateEntityTagsWith: entityTagFactoryBinding content + cachingDirectives: cachingDirectives + allowedLanguageTags: allowedLanguageTags + drivenBy: [ :builder :resource :requestContext :resourceLocation | + builder addAsSelfLink: resourceLocation. + aMediaControlsFactoryBlock + cull: builder + cull: resource + cull: requestContext + cull: resourceLocation. + builder build + ] + handleErrorsWith: exceptionHandler + ] ] { #category : 'building' } RESTfulRequestHandlerBuilder >> build [ - ^ handlerFactory value + ^ requestHandlerFactory value ] { #category : 'configuring' } @@ -159,24 +163,27 @@ RESTfulRequestHandlerBuilder >> handling: anEndpoint locatingSubresourcesWith: a { #category : 'initialization' } RESTfulRequestHandlerBuilder >> initialize [ - super initialize. - decodingRules := Dictionary new. - encodingRules := Dictionary new. - entityTagFactoryBinding := Binding undefinedExplainedBy: 'Missing ETag calculation'. - cachingDirectives := #(). - allowedLanguageTags := OrderedCollection new. - paginationPolicyFactory := [ :requestHandler | RESTfulControllerDoNotPaginateCollectionsPolicy for: requestHandler ]. - exceptionHandler := RESTfulRequestExceptionHandler new. - handlerFactory := [ RESTfulRequestHandler - resourceLocator: resourceLocator - paginationPolicy: paginationPolicyFactory - decodingRules: decodingRules - encodingRules: encodingRules - calculateEntityTagsWith: entityTagFactoryBinding content - cachingDirectives: cachingDirectives - allowedLanguageTags: allowedLanguageTags - handleErrorsWith: exceptionHandler - ] + super initialize. + decodingRules := Dictionary new. + encodingRules := Dictionary new. + entityTagFactoryBinding := Binding undefinedExplainedBy: 'Missing ETag calculation'. + cachingDirectives := #( ). + allowedLanguageTags := OrderedCollection new. + paginationPolicyFactory := [ :requestHandler | + RESTfulControllerDoNotPaginateCollectionsPolicy for: requestHandler ]. + exceptionHandler := RESTfulRequestExceptionHandler new. + requestHandlerClass := RESTfulRequestHandler. + requestHandlerFactory := [ + requestHandlerClass + resourceLocator: resourceLocator + paginationPolicy: paginationPolicyFactory + decodingRules: decodingRules + encodingRules: encodingRules + calculateEntityTagsWith: entityTagFactoryBinding content + cachingDirectives: cachingDirectives + allowedLanguageTags: allowedLanguageTags + handleErrorsWith: exceptionHandler + ] ] { #category : 'private' } @@ -227,43 +234,30 @@ RESTfulRequestHandlerBuilder >> whenResponding: aMediaType encodeApplying: anEnc { #category : 'encoding' } RESTfulRequestHandlerBuilder >> whenResponding: aMediaType encodeToJsonApplying: aBlock [ - self whenResponding: aMediaType encodeApplying: [ :resource :requestContext | - self - jsonFor: resource - within: requestContext - applying: aBlock - writingResourceWith: [ :writer | - writer for: ResourceCollection do: [ :mapping | - mapping - mapInstVar: #items; - mapAsHypermediaControls: [ :collection | - requestContext hypermediaControlsFor: collection items ] - ]. - writer nextPut: resource - ] - ] + self whenResponding: aMediaType encodeApplying: [ :resource :requestContext | + self + jsonFor: resource + within: requestContext + applying: aBlock + writingResourceWith: [ :writer | + requestHandlerClass encode: resource toJsonUsing: writer within: requestContext ] + ] ] { #category : 'encoding' } RESTfulRequestHandlerBuilder >> whenResponding: aMediaType encodeToJsonApplying: aBlock as: schema [ - self whenResponding: aMediaType encodeApplying: [ :resource :requestContext | - self - jsonFor: resource - within: requestContext - applying: aBlock - writingResourceWith: [ :writer | - writer - for: ResourceCollection do: [ :mapping | - ( mapping mapInstVar: #items ) valueSchema: #ResourceCollectionItems. - mapping mapAsHypermediaControls: [ :collection | - requestContext hypermediaControlsFor: collection items ] - ]; - for: #ResourceCollectionItems customDo: [ :mapping | mapping listOfElementSchema: schema ]. - - [ writer nextPut: resource as: schema ] - unless: ( resource isA: ResourceCollection ) - inWhichCase: [ writer nextPut: resource ] - ] - ] + self whenResponding: aMediaType encodeApplying: [ :resource :requestContext | + self + jsonFor: resource + within: requestContext + applying: aBlock + writingResourceWith: [ :writer | + requestHandlerClass + encode: resource + toJsonUsing: writer + as: schema + within: requestContext + ] + ] ] From 3249be3a6cb8fcf4d76db80fb45c44c94aec0f3e Mon Sep 17 00:00:00 2001 From: Gabriel Omar Cotelli Date: Fri, 14 Jun 2024 20:01:55 -0300 Subject: [PATCH 2/2] Improve coverage --- ...ericanCurrenciesRESTfulController.class.st | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/source/Stargate-Examples/SouthAmericanCurrenciesRESTfulController.class.st b/source/Stargate-Examples/SouthAmericanCurrenciesRESTfulController.class.st index 1fce0ac..017885e 100644 --- a/source/Stargate-Examples/SouthAmericanCurrenciesRESTfulController.class.st +++ b/source/Stargate-Examples/SouthAmericanCurrenciesRESTfulController.class.st @@ -137,15 +137,22 @@ SouthAmericanCurrenciesRESTfulController >> initializeBanknotesByCurrency [ { #category : 'initialization' } SouthAmericanCurrenciesRESTfulController >> initializeBanknotesRequestHandler [ - banknotesRequestHandler := RESTfulRequestHandlerBuilder new - handling: 'banknotes' - extractingIdentifierWith: [ :httpRequest | self identifierIn: httpRequest ] - locatingParentResourceWith: currenciesRequestHandler resourceLocator; - whenResponding: ZnMimeType applicationJson - encodeToJsonApplying: [ :resource :requestContext :writer | ]; - createEntityTagHashing: [ :hasher :banknote :requestContext | hasher include: banknote ]; - directCachingWith: [ :caching | caching doNotExpire ]; - build + banknotesRequestHandler := RESTfulRequestHandlerBuilder new + handling: 'banknotes' + extractingIdentifierWith: [ :httpRequest | + self identifierIn: httpRequest ] + locatingParentResourceWith: currenciesRequestHandler resourceLocator; + whenResponding: ZnMimeType applicationJson + encodeToJsonApplying: [ :resource :requestContext :writer | + writer + for: #Banknotes + customDo: [ :mapping | + mapping encoder: [ :banknotes | banknotes ] ] ] + as: #Banknotes; + createEntityTagHashing: [ :hasher :banknote :requestContext | + hasher include: banknote ]; + directCachingWith: [ :caching | caching doNotExpire ]; + build ] { #category : 'initialization' }