From 3c26f448c234c7975959747ca42afdd77dcfb0c5 Mon Sep 17 00:00:00 2001 From: mtabacman Date: Wed, 22 May 2024 16:35:06 -0300 Subject: [PATCH 1/3] Allow using instance creation methods when reading NeoJSON objects --- .../PetOrdersRESTfulControllerTest.class.st | 37 +++++++ .../PetOrdersRESTfulController.class.st | 99 +++++++++++++------ .../NeoJSONReader.extension.st | 20 ++++ .../ValidCompleteInstanceMapping.class.st | 81 +++++++++++++++ 4 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st diff --git a/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st b/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st index 85f98d9..d2e8331 100644 --- a/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st +++ b/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st @@ -48,6 +48,16 @@ PetOrdersRESTfulControllerTest >> createOrder [ within: self newHttpRequestContext ] +{ #category : 'private - support' } +PetOrdersRESTfulControllerTest >> createOverlyComplexOrder [ + + ^ resourceController + createOrderBasedOn: ( self requestToPOSTAsOverlyComplexOrder: + ( '{"date":{"date":{"year":2018,"month":10,"day":24,"offset":0},"time":"18:05:46.418Z"},"pet":"<1p>"}' + expandMacrosWith: self petUrl ) ) + within: self newHttpRequestContext +] + { #category : 'private - support' } PetOrdersRESTfulControllerTest >> getFirstOrderAndWithJsonDo: aBlock [ @@ -117,6 +127,12 @@ PetOrdersRESTfulControllerTest >> requestToPOSTAsOrder: json [ ^ self requestToPOST: json as: resourceController orderVersion1dot0dot0MediaType ] +{ #category : 'private - HTTP requests' } +PetOrdersRESTfulControllerTest >> requestToPOSTAsOverlyComplexOrder: json [ + + ^ self requestToPOST: json as: resourceController overlyComplexOrderVersion1dot0dot0MediaType +] + { #category : 'private - HTTP requests' } PetOrdersRESTfulControllerTest >> requestToPUTComment: aComment on: aSubresourceUrl at: aCommentIndex forOrder: anOrderId conditionalTo: anETag [ @@ -500,6 +516,27 @@ PetOrdersRESTfulControllerTest >> testOrderCreationWhenDecodingFailsDueToMissing raise: HTTPClientError badRequest withMessageText: 'Missing required keys (#pet)' ] +{ #category : 'tests - orders' } +PetOrdersRESTfulControllerTest >> testOverlyComplexOrderCreation [ + + | response order | + + response := self createOverlyComplexOrder. + + self + assert: response isSuccess; + assert: response status equals: 201; + assertUrl: response location equals: 'https://petstore.example.com/orders/1'; + assert: response hasEntity; + assert: orderRepository count equals: 1. + order := orderRepository findAll first. + self + assert: order pet equals: self petUrl; + assert: order date equals: ( DateAndTime + date: ( Date readFrom: '2018-10-24' pattern: 'yyyy-mm-dd' ) + time: ( Time fromString: '18:05:46.418' ) ) +] + { #category : 'tests' } PetOrdersRESTfulControllerTest >> testRoutes [ diff --git a/source/Stargate-Examples/PetOrdersRESTfulController.class.st b/source/Stargate-Examples/PetOrdersRESTfulController.class.st index b89ac9d..6158ec8 100644 --- a/source/Stargate-Examples/PetOrdersRESTfulController.class.st +++ b/source/Stargate-Examples/PetOrdersRESTfulController.class.st @@ -113,32 +113,61 @@ PetOrdersRESTfulController >> completeTemplate [ { #category : 'private' } PetOrdersRESTfulController >> configureOrderDecodingOn: reader [ - ^ reader - for: PetOrder strictDo: [ :mapping | - mapping - mapInstVar: #date; - mapProperty: #pet - setter: [ :order :url | - LanguagePlatform current atInstanceVariableNamed: 'pet' on: order put: url asUrl ] - ]; - nextAs: PetOrder + reader for: #Url customDo: [ :mapping | mapping decoder: [ :string | string asUrl ] ]. + reader for: PetOrder createInstanceUsing: [ :mapping | + mapping + mapProperty: #date; + mapProperty: #pet as: #Url. + mapping mapCreationSending: #for:on: withArguments: { #pet. #date } + ]. + + ^ reader nextAs: PetOrder ] { #category : 'private' } PetOrdersRESTfulController >> configureOrderEncodingOn: writer within: requestContext [ writer + for: DateAndTime + customDo: [ :mapping | mapping encoder: [ :dateAndTime | dateAndTime printString ] ]; for: ZnUrl customDo: [ :mapping | mapping encoder: [ :url | url printString ] ]; - for: #Order - do: [ :mapping | + for: #Order do: [ :mapping | mapping mapProperty: #pet getter: #pet; - mapProperty: #name getter: #date; + mapProperty: #date getter: #date; mapProperty: #status getter: [ :object | requestContext objectUnder: #status ]; mapAsHypermediaControls: [ :order | requestContext hypermediaControlsFor: order ] ] ] +{ #category : 'private' } +PetOrdersRESTfulController >> configureOverlyComplexOrderDecodingOn: reader [ + + reader for: #Url customDo: [ :mapping | mapping decoder: [ :string | string asUrl ] ]. + reader for: #Time customDo: [ :mapping | mapping decoder: [ :string | string asTime ] ]. + reader for: Date createInstanceUsing: [ :mapping | + mapping + mapProperty: #year; + mapProperty: #month; + mapProperty: #day. + mapping mapCreationSending: #newDay:month:year: withArguments: { #day. #month. #year } + ]. + reader for: DateAndTime createInstanceUsing: [ :mapping | + mapping + mapProperty: #date as: Date; + mapProperty: #time as: #Time. + mapping mapCreationSending: #date:time: withArguments: { #date. #time } + ]. + reader for: PetOrder createInstanceUsing: [ :mapping | + mapping + mapProperty: #date as: DateAndTime; + mapProperty: #pet as: #Url. + mapping mapCreationSending: #for:on: withArguments: { #pet. #date } + ]. + + ^ reader nextAs: PetOrder +] + { #category : 'API - comments' } PetOrdersRESTfulController >> createCommentBasedOn: httpRequest within: requestContext [ @@ -316,23 +345,29 @@ PetOrdersRESTfulController >> initializeCommentsRequestHandler [ PetOrdersRESTfulController >> initializeOrdersRequestHandler [ ordersRequestHandler := RESTfulRequestHandlerBuilder new - handling: 'orders' - locatingResourcesWith: [ :order :requestContext | ordersRepository identifierOf: order ] - extractingIdentifierWith: [ :httpRequest | self identifierIn: httpRequest ]; - beHypermediaDrivenBy: - [ :builder :order :requestContext :orderLocation | self affect: builder withMediaControlsFor: order locatedAt: orderLocation ]; - whenAccepting: self orderVersion1dot0dot0MediaType - decodeFromJsonApplying: [ :json :reader | self configureOrderDecodingOn: reader ]; - whenResponding: self orderVersion1dot0dot0MediaType - encodeToJsonApplying: [ :resource :requestContext :writer | self configureOrderEncodingOn: writer within: requestContext ] - as: #Order; - createEntityTagHashing: [ :hasher :order :requestContext | - hasher - include: ( ordersRepository identifierOf: order ); - include: ( ordersRepository lastModificationOf: order ) - ]; - directCachingWith: [ :caching | caching beAvailableFor: 1 minute ]; - build + handling: 'orders' + locatingResourcesWith: [ :order :requestContext | + ordersRepository identifierOf: order ] + extractingIdentifierWith: [ :httpRequest | self identifierIn: httpRequest ]; + beHypermediaDrivenBy: [ :builder :order :requestContext :orderLocation | + self affect: builder withMediaControlsFor: order locatedAt: orderLocation ]; + whenAccepting: self orderVersion1dot0dot0MediaType + decodeFromJsonApplying: [ :json :reader | + self configureOrderDecodingOn: reader ]; + whenAccepting: self overlyComplexOrderVersion1dot0dot0MediaType + decodeFromJsonApplying: [ :json :reader | + self configureOverlyComplexOrderDecodingOn: reader ]; + whenResponding: self orderVersion1dot0dot0MediaType + encodeToJsonApplying: [ :resource :requestContext :writer | + self configureOrderEncodingOn: writer within: requestContext ] + as: #Order; + createEntityTagHashing: [ :hasher :order :requestContext | + hasher + include: ( ordersRepository identifierOf: order ); + include: ( ordersRepository lastModificationOf: order ) + ]; + directCachingWith: [ :caching | caching beAvailableFor: 1 minute ]; + build ] { #category : 'initialization' } @@ -370,6 +405,12 @@ PetOrdersRESTfulController >> orderVersion1dot0dot0MediaType [ ^ self jsonMediaType: 'order' vendoredBy: 'stargate' version: '1.0.0' ] +{ #category : 'private' } +PetOrdersRESTfulController >> overlyComplexOrderVersion1dot0dot0MediaType [ + + ^ self jsonMediaType: 'overly-complex-order' vendoredBy: 'stargate' version: '1.0.0' +] + { #category : 'private' } PetOrdersRESTfulController >> requestHandler [ diff --git a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st index 06fa3f2..53e888d 100644 --- a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st +++ b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st @@ -1,5 +1,15 @@ Extension { #name : 'NeoJSONReader' } +{ #category : '*Stargate-NeoJSON-Extensions' } +NeoJSONReader >> for: schemaName createInstanceUsing: block [ + + | mapping | + + mapping := self validCompleteInstanceMappingFor: schemaName. + block value: mapping. + ^ mapping +] + { #category : '*Stargate-NeoJSON-Extensions' } NeoJSONReader >> for: schemaName strictDo: block [ @@ -19,3 +29,13 @@ NeoJSONReader >> strictMappingFor: smalltalkClass [ yourself ] ] + +{ #category : '*Stargate-NeoJSON-Extensions' } +NeoJSONReader >> validCompleteInstanceMappingFor: smalltalkClass [ + + ^ self mappings at: smalltalkClass ifAbsentPut: [ + ValidCompleteInstanceMapping new + subjectClass: smalltalkClass; + yourself + ] +] diff --git a/source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st b/source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st new file mode 100644 index 0000000..c9f77d7 --- /dev/null +++ b/source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st @@ -0,0 +1,81 @@ +" +I am NeoJSONStrictObjectMapping, I'm equivalent to NeoJSONObjectMapping but more strict. +I will fail on reading properties of an object if some of the mapped properties are missing in the incoming JSON. + +" +Class { + #name : 'ValidCompleteInstanceMapping', + #superclass : 'NeoJSONObjectMapping', + #instVars : [ + 'instanceCreationSelector', + 'argumentNames' + ], + #category : 'Stargate-NeoJSON-Extensions', + #package : 'Stargate-NeoJSON-Extensions' +} + +{ #category : 'private' } +ValidCompleteInstanceMapping >> errorDescriptionForMissing: propertyNames [ + + ^ String streamContents: [ :stream | + stream + nextPutAll: 'Missing required keys'; + space; + nextPut: $(. + propertyNames + do: [ :propertyName | + stream + nextPut: $#; + nextPutAll: propertyName + ] + separatedBy: [ + stream + nextPut: $,; + space + ]. + stream nextPut: $) + ] +] + +{ #category : 'mapping' } +ValidCompleteInstanceMapping >> mapCreationSending: anInstanceCreationSelector withArguments: anArgumentCollection [ + + instanceCreationSelector := anInstanceCreationSelector. + argumentNames := anArgumentCollection +] + +{ #category : 'mapping' } +ValidCompleteInstanceMapping >> mapProperty: aKey [ + + ^ self + mapProperty: aKey + getter: [ :object | ] + setter: [ :arguments :value | arguments at: aKey put: value ] +] + +{ #category : 'mapping' } +ValidCompleteInstanceMapping >> mapProperty: aKey as: aValueSchema [ + + ( self mapProperty: aKey ) valueSchema: aValueSchema +] + +{ #category : 'parsing' } +ValidCompleteInstanceMapping >> readFrom: jsonReader [ + + | argumentByName arguments missingArguments | + + argumentByName := Dictionary new. + jsonReader parseMapKeysDo: [ :key | + ( self propertyNamed: key ifAbsent: [ nil ] ) + ifNil: [ "read, skip & ignore value" jsonReader next ] + ifNotNil: [ :mapping | mapping readObject: argumentByName from: jsonReader ] + ]. + + missingArguments := OrderedCollection new. + arguments := argumentNames collect: [ :argumentName | + argumentByName at: argumentName ifAbsent: [ missingArguments add: argumentName ] ]. + missingArguments ifNotEmpty: [ + jsonReader error: ( self errorDescriptionForMissing: missingArguments ) ]. + + ^ subjectClass perform: instanceCreationSelector withArguments: arguments +] From 6bf5eecd3ffda1da917077b2e9e837536b7df9fb Mon Sep 17 00:00:00 2001 From: mtabacman Date: Fri, 24 May 2024 13:21:27 -0300 Subject: [PATCH 2/3] Changed JSON extension example to prevent incompatibilities with GemStone protocol --- .../PetOrdersRESTfulControllerTest.class.st | 11 ++++---- .../PetOrdersRESTfulController.class.st | 26 +++++++------------ ...ss.st => InstanceCreationMapping.class.st} | 12 ++++----- .../NeoJSONReader.extension.st | 10 +++---- 4 files changed, 26 insertions(+), 33 deletions(-) rename source/Stargate-NeoJSON-Extensions/{ValidCompleteInstanceMapping.class.st => InstanceCreationMapping.class.st} (82%) diff --git a/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st b/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st index d2e8331..fc60e29 100644 --- a/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st +++ b/source/Stargate-Examples-Tests/PetOrdersRESTfulControllerTest.class.st @@ -53,8 +53,7 @@ PetOrdersRESTfulControllerTest >> createOverlyComplexOrder [ ^ resourceController createOrderBasedOn: ( self requestToPOSTAsOverlyComplexOrder: - ( '{"date":{"date":{"year":2018,"month":10,"day":24,"offset":0},"time":"18:05:46.418Z"},"pet":"<1p>"}' - expandMacrosWith: self petUrl ) ) + '{"date":"2018-10-24T18:05:46.418Z","pet":{"alternativeName":"Fido","itsType":"Dog","theStatus":"happy"}}' ) within: self newHttpRequestContext ] @@ -531,10 +530,10 @@ PetOrdersRESTfulControllerTest >> testOverlyComplexOrderCreation [ assert: orderRepository count equals: 1. order := orderRepository findAll first. self - assert: order pet equals: self petUrl; - assert: order date equals: ( DateAndTime - date: ( Date readFrom: '2018-10-24' pattern: 'yyyy-mm-dd' ) - time: ( Time fromString: '18:05:46.418' ) ) + assert: order pet name equals: 'Fido'; + assert: order pet type equals: 'Dog'; + assert: order pet status equals: 'HAPPY'; + assert: order date equals: '2018-10-24T18:05:46.418Z' ] { #category : 'tests' } diff --git a/source/Stargate-Examples/PetOrdersRESTfulController.class.st b/source/Stargate-Examples/PetOrdersRESTfulController.class.st index 6158ec8..30f8e25 100644 --- a/source/Stargate-Examples/PetOrdersRESTfulController.class.st +++ b/source/Stargate-Examples/PetOrdersRESTfulController.class.st @@ -128,8 +128,7 @@ PetOrdersRESTfulController >> configureOrderDecodingOn: reader [ PetOrdersRESTfulController >> configureOrderEncodingOn: writer within: requestContext [ writer - for: DateAndTime - customDo: [ :mapping | mapping encoder: [ :dateAndTime | dateAndTime printString ] ]; + for: Pet do: [ :mapping | mapping mapInstVars ]; for: ZnUrl customDo: [ :mapping | mapping encoder: [ :url | url printString ] ]; for: #Order do: [ :mapping | mapping @@ -143,25 +142,20 @@ PetOrdersRESTfulController >> configureOrderEncodingOn: writer within: requestCo { #category : 'private' } PetOrdersRESTfulController >> configureOverlyComplexOrderDecodingOn: reader [ - reader for: #Url customDo: [ :mapping | mapping decoder: [ :string | string asUrl ] ]. - reader for: #Time customDo: [ :mapping | mapping decoder: [ :string | string asTime ] ]. - reader for: Date createInstanceUsing: [ :mapping | + reader for: #Status customDo: [ :mapping | mapping decoder: [ :string | string asUppercase ] ]. + reader for: Pet createInstanceUsing: [ :mapping | mapping - mapProperty: #year; - mapProperty: #month; - mapProperty: #day. - mapping mapCreationSending: #newDay:month:year: withArguments: { #day. #month. #year } - ]. - reader for: DateAndTime createInstanceUsing: [ :mapping | + mapProperty: #alternativeName; + mapProperty: #itsType; + mapProperty: #theStatus as: #Status. mapping - mapProperty: #date as: Date; - mapProperty: #time as: #Time. - mapping mapCreationSending: #date:time: withArguments: { #date. #time } + mapCreationSending: #named:ofType:withStatus: + withArguments: { #alternativeName. #itsType. #theStatus } ]. reader for: PetOrder createInstanceUsing: [ :mapping | mapping - mapProperty: #date as: DateAndTime; - mapProperty: #pet as: #Url. + mapProperty: #date; + mapProperty: #pet as: Pet. mapping mapCreationSending: #for:on: withArguments: { #pet. #date } ]. diff --git a/source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st b/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st similarity index 82% rename from source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st rename to source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st index c9f77d7..48ffac3 100644 --- a/source/Stargate-NeoJSON-Extensions/ValidCompleteInstanceMapping.class.st +++ b/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st @@ -4,7 +4,7 @@ I will fail on reading properties of an object if some of the mapped properties " Class { - #name : 'ValidCompleteInstanceMapping', + #name : 'InstanceCreationMapping', #superclass : 'NeoJSONObjectMapping', #instVars : [ 'instanceCreationSelector', @@ -15,7 +15,7 @@ Class { } { #category : 'private' } -ValidCompleteInstanceMapping >> errorDescriptionForMissing: propertyNames [ +InstanceCreationMapping >> errorDescriptionForMissing: propertyNames [ ^ String streamContents: [ :stream | stream @@ -38,14 +38,14 @@ ValidCompleteInstanceMapping >> errorDescriptionForMissing: propertyNames [ ] { #category : 'mapping' } -ValidCompleteInstanceMapping >> mapCreationSending: anInstanceCreationSelector withArguments: anArgumentCollection [ +InstanceCreationMapping >> mapCreationSending: anInstanceCreationSelector withArguments: anArgumentCollection [ instanceCreationSelector := anInstanceCreationSelector. argumentNames := anArgumentCollection ] { #category : 'mapping' } -ValidCompleteInstanceMapping >> mapProperty: aKey [ +InstanceCreationMapping >> mapProperty: aKey [ ^ self mapProperty: aKey @@ -54,13 +54,13 @@ ValidCompleteInstanceMapping >> mapProperty: aKey [ ] { #category : 'mapping' } -ValidCompleteInstanceMapping >> mapProperty: aKey as: aValueSchema [ +InstanceCreationMapping >> mapProperty: aKey as: aValueSchema [ ( self mapProperty: aKey ) valueSchema: aValueSchema ] { #category : 'parsing' } -ValidCompleteInstanceMapping >> readFrom: jsonReader [ +InstanceCreationMapping >> readFrom: jsonReader [ | argumentByName arguments missingArguments | diff --git a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st index 53e888d..5a54227 100644 --- a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st +++ b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st @@ -5,7 +5,7 @@ NeoJSONReader >> for: schemaName createInstanceUsing: block [ | mapping | - mapping := self validCompleteInstanceMappingFor: schemaName. + mapping := self instanceCreationMappingFor: schemaName. block value: mapping. ^ mapping ] @@ -21,20 +21,20 @@ NeoJSONReader >> for: schemaName strictDo: block [ ] { #category : '*Stargate-NeoJSON-Extensions' } -NeoJSONReader >> strictMappingFor: smalltalkClass [ +NeoJSONReader >> instanceCreationMappingFor: smalltalkClass [ ^ self mappings at: smalltalkClass ifAbsentPut: [ - NeoJSONStrictObjectMapping new + InstanceCreationMapping new subjectClass: smalltalkClass; yourself ] ] { #category : '*Stargate-NeoJSON-Extensions' } -NeoJSONReader >> validCompleteInstanceMappingFor: smalltalkClass [ +NeoJSONReader >> strictMappingFor: smalltalkClass [ ^ self mappings at: smalltalkClass ifAbsentPut: [ - ValidCompleteInstanceMapping new + NeoJSONStrictObjectMapping new subjectClass: smalltalkClass; yourself ] From db70c1be283719190df8e6843ef8fbe71c3ee947 Mon Sep 17 00:00:00 2001 From: mtabacman Date: Fri, 24 May 2024 15:14:17 -0300 Subject: [PATCH 3/3] Corrections from code review --- .../InstanceCreationMapping.class.st | 2 +- source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st b/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st index 48ffac3..340cd7d 100644 --- a/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st +++ b/source/Stargate-NeoJSON-Extensions/InstanceCreationMapping.class.st @@ -1,5 +1,5 @@ " -I am NeoJSONStrictObjectMapping, I'm equivalent to NeoJSONObjectMapping but more strict. +I am InstanceCreationMapping, I'm equivalent to NeoJSONObjectMapping but require an instance creation method to build instances. I will fail on reading properties of an object if some of the mapped properties are missing in the incoming JSON. " diff --git a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st index 5a54227..c3c1071 100644 --- a/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st +++ b/source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st @@ -1,11 +1,11 @@ Extension { #name : 'NeoJSONReader' } { #category : '*Stargate-NeoJSON-Extensions' } -NeoJSONReader >> for: schemaName createInstanceUsing: block [ +NeoJSONReader >> for: smalltalkClass createInstanceUsing: block [ | mapping | - mapping := self instanceCreationMappingFor: schemaName. + mapping := self instanceCreationMappingFor: smalltalkClass. block value: mapping. ^ mapping ]