Skip to content

Commit

Permalink
Allow using instance creation methods when reading NeoJSON objects
Browse files Browse the repository at this point in the history
  • Loading branch information
mtabacman committed May 22, 2024
1 parent 0de69dc commit 3c26f44
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 [

Expand Down Expand Up @@ -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 [

Expand Down Expand Up @@ -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 [

Expand Down
99 changes: 70 additions & 29 deletions source/Stargate-Examples/PetOrdersRESTfulController.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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 [

Expand Down Expand Up @@ -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' }
Expand Down Expand Up @@ -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 [

Expand Down
20 changes: 20 additions & 0 deletions source/Stargate-NeoJSON-Extensions/NeoJSONReader.extension.st
Original file line number Diff line number Diff line change
@@ -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 [

Expand All @@ -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
]
]
Original file line number Diff line number Diff line change
@@ -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
]

0 comments on commit 3c26f44

Please sign in to comment.