From edc77dc36778cf99a1e16116a5c94e4464c76853 Mon Sep 17 00:00:00 2001 From: Gabriel Omar Cotelli Date: Tue, 9 Oct 2018 17:23:31 -0300 Subject: [PATCH 1/2] initial template --- .gitattributes | 1 + .project | 3 +++ .smalltalk.ston | 15 +++++++++++++++ .travis.yml | 26 ++++++++++++++++++++++++++ CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++++ LICENSE | 2 +- README.md | 38 +++++++++++++++++++++++++++++++++++++- docs/Installation.md | 38 ++++++++++++++++++++++++++++++++++++++ source/.properties | 3 +++ 9 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 .project create mode 100644 .smalltalk.ston create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 docs/Installation.md create mode 100644 source/.properties diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..da0f990 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.st linguist-language=Smalltalk diff --git a/.project b/.project new file mode 100644 index 0000000..d7c7acd --- /dev/null +++ b/.project @@ -0,0 +1,3 @@ +{ + 'srcDirectory' : 'source' +} diff --git a/.smalltalk.ston b/.smalltalk.ston new file mode 100644 index 0000000..0b8713b --- /dev/null +++ b/.smalltalk.ston @@ -0,0 +1,15 @@ +SmalltalkCISpec { + #loading : [ + SCIMetacelloLoadSpec { + #baseline : 'Stargate', + #directory : 'source', + #load : [ 'Development' ], + #platforms : [ #pharo ] + } + ], + #testing : { + #coverage : { + #packages : [ 'Stargate*' ] + } + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..23bb217 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: smalltalk +sudo: false +os: +- linux +smalltalk: +- Pharo64-7.0 +- Pharo-7.0 +- Pharo-6.1 +matrix: + allow_failures: + - smalltalk: Pharo64-7.0 + - smalltalk: Pharo-7.0 + fast_finish: true +before_deploy: + - cp "${SMALLTALK_CI_IMAGE}" "Stargate.image" + - cp "${SMALLTALK_CI_CHANGES}" "Stargate.changes" + - zip -q "${TRAVIS_BRANCH}-${TRAVIS_SMALLTALK_VERSION}.zip" "Stargate.image" "Stargate.changes" +deploy: + provider: releases + api_key: + secure: TwpzX3XVrSLLzCMAWmTJyA1qBXdutY/adojcQX1RXb86jTbAhMJwi3Iv8xDhAT5Aa5lKpNOLBqIfS8GG8W0lt++f5RnI2S8RHSm4HtctjTc9uRhBK4qsnlU24WsyFgsvhURsA/NaPnWAqSnJBivoYKzob7b5rzAsElr7ofelisT+pQthzamNqkNg/NQW4nCJpkkMRbAAig1O3l8jA7l0DE/2XTXTzS87LTIi5wlmfX4N94dG5F3vmj9i1DSZGFURNgZxdUV80uNmabmZxtAscrwEJJgAcEBbPvSu9cLgUg/2vqFXDaY+ksD0euEsd6bOkcadBUSc28i9YZ5GX7PM/FJgXB2wfylb/PnqTiRkte7xIByGndBgIJcE9EujjNfxQl0j4GmTfK7dyHd5wxXS/n2Zbz/t3UutNaI3AqWb5BA8rEw3ri37Vh+sCiEEU60RzSocq6bOSXHXH8+HfcCnX26WYwQEgoe7Hbe9kGCmCt6prDQUUtC8St1DUac5ri+uc/7+g8HrBemps3miK3hxzTHDFbo4T322OTYniE7CkuXuUU/WzvFbyrjU+AJ0c1EqyTY8VJhii2U1zmNPkG2SYQyKm0gRvht8wjj07QGe+Ml62CHj9QIUuaSVrVAyZQYBfjfAJ7j8jS8kBl7ywqw7yM0v8Qr7Dytspw4qA1Hs50I= + file: "${TRAVIS_BRANCH}-${TRAVIS_SMALLTALK_VERSION}.zip" + skip_cleanup: true + on: + repo: ba-st/Stargate + tags: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..694babf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +Contributing +============ + +There's several ways to contribute to the project: reporting bugs, sending feedback, proposing ideas for new features, fixing or adding documentation, promoting the project, or even contributing code. + +## Reporting issues + +You can report issues [here](https://github.com/ba-st/Stargate/issues/new) + +## Contributing Code +- This project is MIT licensed, so any code contribution MUST be under the same license. +- This project uses [Semantic Versioning](http://semver.org/), so keep it in mind when you make backwards-incompatible changes. If some backwards incompatible change is made the major version MUST be increased. +- The source code is hosted in this repository using the Tonel format in the `source` folder. +- The master branch contains the latest changes and should always be in a releasable state. +- Feel free to send pull requests or fork the project. +- Code contributions without test cases have a lower probability of being merged into the main branch. + +### Using Iceberg +1. Download a [Pharo Image and VM](https://get.pharo.org/64) +2. Clone the project or your fork using Iceberg +3. Open the Working Copy and using the contextual menu select `Metacello -> Install baseline...` +4. Input `Development` +5. This will load the base code and the test cases +6. Create a new branch to host your code changes +7. Do the changes +8. Run the test cases +9. Commit and push your changes to the branch using the Iceberg UI +10. Create a Pull Request against the master branch + +## Contributing documentation + +The project documentation is maintained in this repository in the `docs` folder and licensed under CC BY-SA 4.0. To contribute some documentation or improve the existing, feel free to create a branch or fork this repository, make your changes and send a pull request. diff --git a/LICENSE b/LICENSE index 3e75b05..1f75c9b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Buenos Aires Smalltalk +Copyright (c) 2018 Buenos Aires Smalltalk Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dbfaedb..397fc2c 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# Stargate \ No newline at end of file +

+

Stargate

+

+ What is this thing? “the motto” and the goals. The vision. +
+ Explore the docs » +
+
+ Report a defect + | + Request feature +

+

+ +[![GitHub release](https://img.shields.io/github/release/ba-st/Stargate.svg)](https://github.com/ba-st/Stargate/releases/latest) +[![Build Status](https://travis-ci.org/ba-st/Stargate.svg?branch=master)](https://travis-ci.org/ba-st/Stargate) +[![Coverage Status](https://coveralls.io/repos/github/ba-st/Stargate/badge.svg?branch=master)](https://coveralls.io/github/ba-st/Stargate?branch=master) + +Why would I care about this thing? When to use, for whom is designed, when not to use. + +## License +- The code is licensed under [MIT](LICENSE). +- The documentation is licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). + +## Quick Start + +- Download the latest [Pharo 32](https://get.pharo.org/) or [64 bits VM](https://get.pharo.org/64/). +- Download a ready to use image from the [release page](https://github.com/ba-st/Stargate/releases/latest) +- Explore the [documentation](docs/) + +## Installation + +To load the project in a Pharo image, or declare it as a dependency of your own project follow this [instructions](docs/Installation.md). + +## Contributing + +Check the [Contribution Guidelines](CONTRIBUTING.md) diff --git a/docs/Installation.md b/docs/Installation.md new file mode 100644 index 0000000..8cea8c6 --- /dev/null +++ b/docs/Installation.md @@ -0,0 +1,38 @@ +# Installation + +## Basic Installation + +You can load **Stargate** evaluating: +```smalltalk +Metacello new + baseline: 'Stargate'; + repository: 'github://ba-st/Stargate:master/source'; + load. +``` +> Change `master` to some released version if you want a pinned version + +## Using as dependency + +In order to include **Stargate** as part of your project, you should reference the package in your product baseline: + +```smalltalk +setUpDependencies: spec + + spec + baseline: 'Stargate' + with: [ spec + repository: 'github://ba-st/Stargate:v{XX}/source'; + loads: #('Deployment') ]; + import: 'Stargate'. +``` +> Replace `{XX}` with the version you want to depend on + +```smalltalk +baseline: spec + + + spec + for: #common + do: [ self setUpDependencies: spec. + spec package: 'My-Package' with: [ spec requires: #('Stargate') ] ] +``` diff --git a/source/.properties b/source/.properties new file mode 100644 index 0000000..53a5454 --- /dev/null +++ b/source/.properties @@ -0,0 +1,3 @@ +{ + #format : #tonel +} From 0b0ed083cdc36fc7fbf90cf273c4f2e6b4f69f95 Mon Sep 17 00:00:00 2001 From: Gabriel Omar Cotelli Date: Tue, 9 Oct 2018 17:45:59 -0300 Subject: [PATCH 2/2] Separating some basic code from Cosmos --- .../BaselineOfStargate.class.st | 34 +++ source/BaselineOfStargate/package.st | 1 + ...sOriginResourceSharingHandlerTest.class.st | 21 ++ .../DynamicDecoderTest.class.st | 32 +++ .../HTTPClientErrorTest.class.st | 44 ++++ .../IsUUIDTest.class.st | 28 ++ .../MappingRuleSetBuilderTest.class.st | 115 ++++++++ .../MappingRuleSetTest.class.st | 249 ++++++++++++++++++ .../PetsWebService.class.st | 44 ++++ .../PetsWebServiceSpecification.class.st | 51 ++++ .../ReflectiveRoutesConfiguratorTest.class.st | 20 ++ .../RouteSpecificationTest.class.st | 21 ++ .../Teapot.extension.st | 7 + source/Stargate-REST-API-Tests/package.st | 1 + .../CorsAwareRouteSpecification.class.st | 32 +++ ...CrossOriginResourceSharingHandler.class.st | 43 +++ .../Stargate-REST-API/DecodingFailed.class.st | 5 + .../Stargate-REST-API/DynamicDecoder.class.st | 47 ++++ .../HTTPClientError.class.st | 49 ++++ .../HttpRequestContext.class.st | 27 ++ source/Stargate-REST-API/IsUUID.class.st | 17 ++ .../MappingNotFound.class.st | 5 + source/Stargate-REST-API/MappingRule.class.st | 72 +++++ .../Stargate-REST-API/MappingRuleSet.class.st | 126 +++++++++ .../MappingRuleSetBuilder.class.st | 148 +++++++++++ .../ReflectiveMappingRuleSetBuilder.class.st | 36 +++ .../ReflectiveRoutesConfigurator.class.st | 43 +++ .../RouteConfigurator.class.st | 65 +++++ .../RouteSpecification.class.st | 54 ++++ source/Stargate-REST-API/UUID.extension.st | 7 + .../WebServiceSpecification.class.st | 5 + .../Stargate-REST-API/ZnEntity.extension.st | 7 + .../ZnStringEntity.extension.st | 9 + source/Stargate-REST-API/package.st | 1 + 34 files changed, 1466 insertions(+) create mode 100644 source/BaselineOfStargate/BaselineOfStargate.class.st create mode 100644 source/BaselineOfStargate/package.st create mode 100644 source/Stargate-REST-API-Tests/CrossOriginResourceSharingHandlerTest.class.st create mode 100644 source/Stargate-REST-API-Tests/DynamicDecoderTest.class.st create mode 100644 source/Stargate-REST-API-Tests/HTTPClientErrorTest.class.st create mode 100644 source/Stargate-REST-API-Tests/IsUUIDTest.class.st create mode 100644 source/Stargate-REST-API-Tests/MappingRuleSetBuilderTest.class.st create mode 100644 source/Stargate-REST-API-Tests/MappingRuleSetTest.class.st create mode 100644 source/Stargate-REST-API-Tests/PetsWebService.class.st create mode 100644 source/Stargate-REST-API-Tests/PetsWebServiceSpecification.class.st create mode 100644 source/Stargate-REST-API-Tests/ReflectiveRoutesConfiguratorTest.class.st create mode 100644 source/Stargate-REST-API-Tests/RouteSpecificationTest.class.st create mode 100644 source/Stargate-REST-API-Tests/Teapot.extension.st create mode 100644 source/Stargate-REST-API-Tests/package.st create mode 100644 source/Stargate-REST-API/CorsAwareRouteSpecification.class.st create mode 100644 source/Stargate-REST-API/CrossOriginResourceSharingHandler.class.st create mode 100644 source/Stargate-REST-API/DecodingFailed.class.st create mode 100644 source/Stargate-REST-API/DynamicDecoder.class.st create mode 100644 source/Stargate-REST-API/HTTPClientError.class.st create mode 100644 source/Stargate-REST-API/HttpRequestContext.class.st create mode 100644 source/Stargate-REST-API/IsUUID.class.st create mode 100644 source/Stargate-REST-API/MappingNotFound.class.st create mode 100644 source/Stargate-REST-API/MappingRule.class.st create mode 100644 source/Stargate-REST-API/MappingRuleSet.class.st create mode 100644 source/Stargate-REST-API/MappingRuleSetBuilder.class.st create mode 100644 source/Stargate-REST-API/ReflectiveMappingRuleSetBuilder.class.st create mode 100644 source/Stargate-REST-API/ReflectiveRoutesConfigurator.class.st create mode 100644 source/Stargate-REST-API/RouteConfigurator.class.st create mode 100644 source/Stargate-REST-API/RouteSpecification.class.st create mode 100644 source/Stargate-REST-API/UUID.extension.st create mode 100644 source/Stargate-REST-API/WebServiceSpecification.class.st create mode 100644 source/Stargate-REST-API/ZnEntity.extension.st create mode 100644 source/Stargate-REST-API/ZnStringEntity.extension.st create mode 100644 source/Stargate-REST-API/package.st diff --git a/source/BaselineOfStargate/BaselineOfStargate.class.st b/source/BaselineOfStargate/BaselineOfStargate.class.st new file mode 100644 index 0000000..e9f066a --- /dev/null +++ b/source/BaselineOfStargate/BaselineOfStargate.class.st @@ -0,0 +1,34 @@ +Class { + #name : #BaselineOfStargate, + #superclass : #BaselineOf, + #category : #BaselineOfStargate +} + +{ #category : #baselines } +BaselineOfStargate >> baseline: spec [ + + + spec + for: #common + do: [ self setUpDependencies: spec. + spec package: 'Stargate-REST-API' with: [ spec requires: #('Buoy' 'Teapot') ]. + spec package: 'Stargate-REST-API-Tests' with: [ spec requires: #('Stargate-REST-API') ]. + spec + group: 'Deployment' with: #('Stargate-REST-API'); + group: 'Development' with: #('Deployment' 'Stargate-REST-API-Tests'); + group: 'default' with: #('Deployment') ] +] + +{ #category : #baselines } +BaselineOfStargate >> setUpDependencies: spec [ + + spec + baseline: 'Buoy' with: [ spec repository: 'github://ba-st/Buoy:v4/source' ]; + import: 'Buoy'. + + spec + configuration: 'Teapot' + with: [ spec + versionString: #stable; + repository: 'http://smalltalkhub.com/mc/zeroflag/Teapot/main/' ] +] diff --git a/source/BaselineOfStargate/package.st b/source/BaselineOfStargate/package.st new file mode 100644 index 0000000..5100385 --- /dev/null +++ b/source/BaselineOfStargate/package.st @@ -0,0 +1 @@ +Package { #name : #BaselineOfStargate } diff --git a/source/Stargate-REST-API-Tests/CrossOriginResourceSharingHandlerTest.class.st b/source/Stargate-REST-API-Tests/CrossOriginResourceSharingHandlerTest.class.st new file mode 100644 index 0000000..3de2fb8 --- /dev/null +++ b/source/Stargate-REST-API-Tests/CrossOriginResourceSharingHandlerTest.class.st @@ -0,0 +1,21 @@ +Class { + #name : #CrossOriginResourceSharingHandlerTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +CrossOriginResourceSharingHandlerTest >> testValue [ + + | handler response | + + handler := CrossOriginResourceSharingHandler allowing: #('GET' 'POST'). + response := handler value: (ZnRequest options: 'url'). + + self + assert: response code equals: 204; + assert: (response headers at: 'Access-Control-Max-Age') equals: '86400'; + assert: (response headers at: 'Access-Control-Allow-Headers') + equals: 'Access-Control-Allow-Origin, Content-Type, Accept'; + assert: (response headers at: 'Access-Control-Allow-Methods') equals: 'GET, POST' +] diff --git a/source/Stargate-REST-API-Tests/DynamicDecoderTest.class.st b/source/Stargate-REST-API-Tests/DynamicDecoderTest.class.st new file mode 100644 index 0000000..57afa68 --- /dev/null +++ b/source/Stargate-REST-API-Tests/DynamicDecoderTest.class.st @@ -0,0 +1,32 @@ +Class { + #name : #DynamicDecoderTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +DynamicDecoderTest >> testDecoding [ + + | decoder aJson decoded | + + decoder := (DynamicDecoder determiningDecoderBy: #type) + decoding: #number applying: [ :json | (json at: #amount) asNumber ]; + decoding: #string applying: [ :json | json at: #name ]; + yourself. + + aJson := '{ + "type" : "number", + "amount" : "5" +}'. + + decoded := decoder value: (STONJSON fromString: aJson). + self assert: decoded equals: 5. + + aJson := '{ + "type" : "string", + "name" : "a string" +}'. + + decoded := decoder value: (STONJSON fromString: aJson). + self assert: decoded equals: 'a string' +] diff --git a/source/Stargate-REST-API-Tests/HTTPClientErrorTest.class.st b/source/Stargate-REST-API-Tests/HTTPClientErrorTest.class.st new file mode 100644 index 0000000..e6a4a56 --- /dev/null +++ b/source/Stargate-REST-API-Tests/HTTPClientErrorTest.class.st @@ -0,0 +1,44 @@ +" +A HTTPClientErrorTest is a test class for testing the behavior of HTTPClientError +" +Class { + #name : #HTTPClientErrorTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +HTTPClientErrorTest >> test404 [ + + self + should: [ HTTPClientError signalNotFound ] + raise: HTTPClientError + withExceptionDo: [ :signal | + self + assert: signal code equals: 404; + assert: signal messageText equals: 'Not found' ] +] + +{ #category : #tests } +HTTPClientErrorTest >> test409 [ + + self + should: [ HTTPClientError signalConflict: 'Sigmund Freud' ] + raise: HTTPClientError + withExceptionDo: [ :signal | + self + assert: signal code equals: 409; + assert: signal messageText equals: 'Sigmund Freud' ] +] + +{ #category : #tests } +HTTPClientErrorTest >> testCode [ + + self + should: [ HTTPClientError signal: 404 describedBy: 'Not Found' ] + raise: HTTPClientError + withExceptionDo: [ :signal | + self + assert: signal code equals: 404; + assert: signal messageText equals: 'Not Found' ] +] diff --git a/source/Stargate-REST-API-Tests/IsUUIDTest.class.st b/source/Stargate-REST-API-Tests/IsUUIDTest.class.st new file mode 100644 index 0000000..584c4a5 --- /dev/null +++ b/source/Stargate-REST-API-Tests/IsUUIDTest.class.st @@ -0,0 +1,28 @@ +Class { + #name : #IsUUIDTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +IsUUIDTest >> testMatches [ + + self + assert: (IsUUID matchesTo: '3wkyfiioh6vu12497yj6g20p2'); + assert: (IsUUID matchesTo: '5l0vw3e9434a49gz49hqz1xig'); + deny: (IsUUID matchesTo: 'ContainsCaps'); + deny: (IsUUID matchesTo: '-containsInvalidChars') +] + +{ #category : #tests } +IsUUIDTest >> testParseString [ + + | first second | + + first := UUID fromString: '0608b9dc-02e4-4dd0-9f8a-ea45160df641'. + second := UUID fromString: 'e85ae7ba-3ca3-4bae-9f62-cc2ce51c525e'. + + self + assert: (IsUUID parseString: first asString36) equals: first; + assert: (IsUUID parseString: second asString36) equals: second +] diff --git a/source/Stargate-REST-API-Tests/MappingRuleSetBuilderTest.class.st b/source/Stargate-REST-API-Tests/MappingRuleSetBuilderTest.class.st new file mode 100644 index 0000000..3d44407 --- /dev/null +++ b/source/Stargate-REST-API-Tests/MappingRuleSetBuilderTest.class.st @@ -0,0 +1,115 @@ +Class { + #name : #MappingRuleSetBuilderTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #accessing } +MappingRuleSetBuilderTest >> applicationJsonVersion1dot0dot0 [ + + ^ ZnMimeType fromString: 'application/json;version=1.0.0' +] + +{ #category : #accessing } +MappingRuleSetBuilderTest >> applicationJsonVersion1dot0dot1 [ + + ^ ZnMimeType fromString: 'application/json;version=1.0.1' +] + +{ #category : #tests } +MappingRuleSetBuilderTest >> testAddingDecoderForAlreadyAddedMimeTypeFails [ + + | mappingRegistry | + + mappingRegistry := MappingRuleSetBuilder new. + + mappingRegistry + addRuleToDecode: ZnMimeType textPlain + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + self + should: [ mappingRegistry + addRuleToDecode: ZnMimeType textPlain + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0 ] + raise: ConflictingObjectFound +] + +{ #category : #tests } +MappingRuleSetBuilderTest >> testAddingEncoderForAlreadyAddedMimeTypeFails [ + + | mappingRegistry | + + mappingRegistry := MappingRuleSetBuilder new. + + mappingRegistry + addRuleToEncode: #triggers + to: ZnMimeType textPlain + using: self triggerJsonEncoderVersion1dot0dot0. + + self + should: [ mappingRegistry + addRuleToEncode: #triggers + to: ZnMimeType textPlain + using: self triggerJsonEncoderVersion1dot0dot0 ] + raise: ConflictingObjectFound +] + +{ #category : #tests } +MappingRuleSetBuilderTest >> testBuilding [ + + | mappingRuleSetBuilder | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self applicationJsonVersion1dot0dot1 + to: #trigger + using: self triggerJsonDecoderVersion1dot0dot1. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #trigger + using: self triggerJsonDecoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToEncode: #trigger + to: self applicationJsonVersion1dot0dot0 + using: self triggerJsonDecoderVersion1dot0dot0. + + self shouldnt: [ mappingRuleSetBuilder build ] raise: Error +] + +{ #category : #tests } +MappingRuleSetBuilderTest >> testBuildingFailsBecauseMustProvideDefault [ + + | mappingRuleSetBuilder | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + self should: [ mappingRuleSetBuilder build ] raise: AssertionFailed +] + +{ #category : #accessing } +MappingRuleSetBuilderTest >> triggerJsonDecoderVersion1dot0dot0 [ + + ^ #triggerJsonDecoderVersion1dot0dot0 +] + +{ #category : #accessing } +MappingRuleSetBuilderTest >> triggerJsonDecoderVersion1dot0dot1 [ + + ^ #triggerJsonDecoderVersion1dot0dot1 +] + +{ #category : #accessing } +MappingRuleSetBuilderTest >> triggerJsonEncoderVersion1dot0dot0 [ + + ^ #triggerJsonEncoderVersion1dot0dot0 +] diff --git a/source/Stargate-REST-API-Tests/MappingRuleSetTest.class.st b/source/Stargate-REST-API-Tests/MappingRuleSetTest.class.st new file mode 100644 index 0000000..7514ebc --- /dev/null +++ b/source/Stargate-REST-API-Tests/MappingRuleSetTest.class.st @@ -0,0 +1,249 @@ +Class { + #name : #MappingRuleSetTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #'accessing - media types' } +MappingRuleSetTest >> applicationJson [ + + ^ ZnMimeType applicationJson +] + +{ #category : #'accessing - media types' } +MappingRuleSetTest >> applicationJsonVersion1dot0dot0 [ + + ^ ZnMimeType fromString: 'application/json; version=1.0.0' +] + +{ #category : #'accessing - media types' } +MappingRuleSetTest >> applicationJsonVersion1dot0dot1 [ + + ^ ZnMimeType fromString: 'application/json; version=1.0.1' +] + +{ #category : #'accessing - media types' } +MappingRuleSetTest >> applicationJsonVersion1dot1dot0 [ + + ^ ZnMimeType fromString: 'application/json; version=1.1.0' +] + +{ #category : #accessing } +MappingRuleSetTest >> keyRepresentingTriggers [ + + ^ #triggers +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingDecodingRuleByAnyMediaTypeGivesDefault [ + + | mappingRuleSetBuilder mappingRuleSet decodingRule | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self applicationJsonVersion1dot0dot1 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + decodingRule := mappingRuleSet ruleToDecode: ZnMimeType any to: #triggers. + self + assert: decodingRule mediaType equals: self applicationJsonVersion1dot0dot1; + assert: decodingRule objectType equals: #triggers +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingDecodingRuleByMediaTypeSpecificVersion [ + + | mappingRuleSetBuilder mappingRuleSet decodingRule | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self applicationJsonVersion1dot0dot1 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + decodingRule := mappingRuleSet + ruleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers. + self + assert: decodingRule mediaType equals: self applicationJsonVersion1dot0dot0; + assert: decodingRule objectType equals: #triggers +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingDecodingRuleByMediaTypeWithoutVersionGivesDefault [ + + | mappingRuleSetBuilder mappingRuleSet decodingRule | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self applicationJsonVersion1dot0dot1 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + decodingRule := mappingRuleSet ruleToDecode: self applicationJson to: #triggers. + self + assert: decodingRule mediaType equals: self applicationJsonVersion1dot0dot1; + assert: decodingRule objectType equals: #triggers +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingDecodingRuleByNotRegisteredMediaTypeGivesObjectNotFound [ + + | mappingRuleSetBuilder mappingRuleSet | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self textPlain + to: #triggers + using: self triggerTextDecoder. + + mappingRuleSet := mappingRuleSetBuilder build. + + self + should: [ mappingRuleSet + ruleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers ] + raise: MappingNotFound +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingDecodingRuleByNotRegisteredSpecificVersionGivesObjectNotFound [ + + | mappingRuleSetBuilder mappingRuleSet | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToDecode: self applicationJsonVersion1dot0dot0 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToDecode: self applicationJsonVersion1dot0dot1 + to: #triggers + using: self triggerJsonDecoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + self + should: [ mappingRuleSet + ruleToDecode: self applicationJsonVersion1dot1dot0 + to: #triggers ] + raise: MappingNotFound +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingEncodingRuleByAnyMediaTypeGivesDefault [ + + | mappingRuleSetBuilder mappingRuleSet encodingRule | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToEncode: self keyRepresentingTriggers + to: self applicationJsonVersion1dot0dot0 + using: self triggerJsonEncoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToEncode: self keyRepresentingTriggers + to: self applicationJsonVersion1dot0dot1 + using: self triggerJsonEncoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + encodingRule := mappingRuleSet ruleToEncode: self keyRepresentingTriggers to: ZnMimeType any. + + self + assert: encodingRule mediaType equals: self applicationJsonVersion1dot0dot1; + assert: encodingRule objectType equals: self keyRepresentingTriggers +] + +{ #category : #tests } +MappingRuleSetTest >> testQueryingEncodingRuleByMediaTypeSpecificVersion [ + + | mappingRuleSetBuilder mappingRuleSet encodingRule | + + mappingRuleSetBuilder := MappingRuleSetBuilder new. + + mappingRuleSetBuilder + addRuleToEncode: self keyRepresentingTriggers + to: self applicationJsonVersion1dot0dot0 + using: self triggerJsonEncoderVersion1dot0dot0. + + mappingRuleSetBuilder + addDefaultRuleToEncode: self keyRepresentingTriggers + to: self applicationJsonVersion1dot0dot1 + using: self triggerJsonEncoderVersion1dot0dot1. + + mappingRuleSet := mappingRuleSetBuilder build. + + encodingRule := mappingRuleSet + ruleToEncode: self keyRepresentingTriggers + to: self applicationJsonVersion1dot0dot0. + + self + assert: encodingRule mediaType equals: self applicationJsonVersion1dot0dot0; + assert: encodingRule objectType equals: self keyRepresentingTriggers +] + +{ #category : #'accessing - media types' } +MappingRuleSetTest >> textPlain [ + + ^ ZnMimeType fromString: 'text/plain;charset=utf-8' +] + +{ #category : #'accessing - enconders and decoders' } +MappingRuleSetTest >> triggerJsonDecoderVersion1dot0dot0 [ + + ^ #triggerJsonDecoderVersion1dot0dot0 +] + +{ #category : #'accessing - enconders and decoders' } +MappingRuleSetTest >> triggerJsonDecoderVersion1dot0dot1 [ + + ^ #triggerJsonDecoderVersion1dot0dot1 +] + +{ #category : #'accessing - enconders and decoders' } +MappingRuleSetTest >> triggerJsonEncoderVersion1dot0dot0 [ + + ^ #triggerJsonEncoderVersion1dot0dot0 +] + +{ #category : #'accessing - enconders and decoders' } +MappingRuleSetTest >> triggerJsonEncoderVersion1dot0dot1 [ + + ^ #triggerJsonEncoderVersion1dot0dot1 +] + +{ #category : #'accessing - enconders and decoders' } +MappingRuleSetTest >> triggerTextDecoder [ + + ^ #triggerTextDecoder +] diff --git a/source/Stargate-REST-API-Tests/PetsWebService.class.st b/source/Stargate-REST-API-Tests/PetsWebService.class.st new file mode 100644 index 0000000..c6b98e7 --- /dev/null +++ b/source/Stargate-REST-API-Tests/PetsWebService.class.st @@ -0,0 +1,44 @@ +Class { + #name : #PetsWebService, + #superclass : #Object, + #instVars : [ + 'mappingRuleSet', + 'pets' + ], + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #'instance creation' } +PetsWebService class >> managing: aPetSet [ + + ^ self new initializeManaging: aPetSet +] + +{ #category : #'encoding and decoding' } +PetsWebService >> decode: aJSON encodedAs: aMediaType to: aKeyRepresentingObjectType within: aContext [ + + ^ (mappingRuleSet ruleToDecode: aMediaType to: aKeyRepresentingObjectType) + applyOn: aJSON + within: aContext +] + +{ #category : #'encoding and decoding' } +PetsWebService >> encode: anObject of: aKeyRepresentingObjectType to: aMediaType within: aContext [ + + ^ (mappingRuleSet ruleToEncode: aKeyRepresentingObjectType to: aMediaType) + applyOn: anObject + within: aContext +] + +{ #category : #initialization } +PetsWebService >> initializeManaging: aPetSet [ + + pets := aPetSet. + mappingRuleSet := (ReflectiveMappingRuleSetBuilder for: self specification) build +] + +{ #category : #accessing } +PetsWebService >> specification [ + + ^ PetsWebServiceSpecification new +] diff --git a/source/Stargate-REST-API-Tests/PetsWebServiceSpecification.class.st b/source/Stargate-REST-API-Tests/PetsWebServiceSpecification.class.st new file mode 100644 index 0000000..46f9b2d --- /dev/null +++ b/source/Stargate-REST-API-Tests/PetsWebServiceSpecification.class.st @@ -0,0 +1,51 @@ +Class { + #name : #PetsWebServiceSpecification, + #superclass : #WebServiceSpecification, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #'mapping rules' } +PetsWebServiceSpecification >> addJsonEncoderVersion1dot0dot0MappingIn: aBuilder [ + + aBuilder + addDefaultRuleToEncode: #pets + to: self applicationJsonVersion1dot0dot0 + using: self petJsonEncoderVersion1dot0dot0 +] + +{ #category : #'accessing - media types' } +PetsWebServiceSpecification >> applicationJsonVersion1dot0dot0 [ + + ^ ZnMimeType fromString: 'application/json;version=1.0.0' +] + +{ #category : #routes } +PetsWebServiceSpecification >> createPetRoute [ + + ^ RouteSpecification handling: #POST at: '/pets' sending: #createPetBasedOn:within: +] + +{ #category : #routes } +PetsWebServiceSpecification >> getPetsRoute [ + + | route | + + route := RouteSpecification handling: #GET at: '/pets' sending: #getPetsBasedOn:within:. + + ^ route asCorsAware +] + +{ #category : #'mapping rules' } +PetsWebServiceSpecification >> petJsonEncoderVersion1dot0dot0 [ + + ^ [ :point | + String + streamContents: [ :stream | + (NeoJSONWriter on: stream) + for: Point + do: [ :mapping | + mapping + mapAccessor: #x; + mapAccessor: #y ]; + nextPut: point ] ] +] diff --git a/source/Stargate-REST-API-Tests/ReflectiveRoutesConfiguratorTest.class.st b/source/Stargate-REST-API-Tests/ReflectiveRoutesConfiguratorTest.class.st new file mode 100644 index 0000000..ecc1c4c --- /dev/null +++ b/source/Stargate-REST-API-Tests/ReflectiveRoutesConfiguratorTest.class.st @@ -0,0 +1,20 @@ +Class { + #name : #ReflectiveRoutesConfiguratorTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +ReflectiveRoutesConfiguratorTest >> testAddingRoutes [ + + | teapot webService | + + teapot := Teapot on. + + webService := PetsWebService managing: OrderedCollection new. + + (ReflectiveRoutesConfigurator appliedTo: teapot) + addRoutesOf: webService. + + self assert: teapot routes size equals: 3 +] diff --git a/source/Stargate-REST-API-Tests/RouteSpecificationTest.class.st b/source/Stargate-REST-API-Tests/RouteSpecificationTest.class.st new file mode 100644 index 0000000..94d3261 --- /dev/null +++ b/source/Stargate-REST-API-Tests/RouteSpecificationTest.class.st @@ -0,0 +1,21 @@ +Class { + #name : #RouteSpecificationTest, + #superclass : #TestCase, + #category : #'Stargate-REST-API-Tests' +} + +{ #category : #tests } +RouteSpecificationTest >> testInstanceCreationAndAccessing [ + + | route | + + route := RouteSpecification + handling: #GET + at: '/pets' + sending: #getPetsBasedOn:within:. + + self + assert: route httpMethod equals: #GET; + assert: route resourceLocation equals: '/pets'; + assert: route message equals: #getPetsBasedOn:within: +] diff --git a/source/Stargate-REST-API-Tests/Teapot.extension.st b/source/Stargate-REST-API-Tests/Teapot.extension.st new file mode 100644 index 0000000..b5c8c72 --- /dev/null +++ b/source/Stargate-REST-API-Tests/Teapot.extension.st @@ -0,0 +1,7 @@ +Extension { #name : #Teapot } + +{ #category : #'*Stargate-REST-API-Tests' } +Teapot >> routes [ + + ^ dynamicRouter routes +] diff --git a/source/Stargate-REST-API-Tests/package.st b/source/Stargate-REST-API-Tests/package.st new file mode 100644 index 0000000..2e4ecbf --- /dev/null +++ b/source/Stargate-REST-API-Tests/package.st @@ -0,0 +1 @@ +Package { #name : #'Stargate-REST-API-Tests' } diff --git a/source/Stargate-REST-API/CorsAwareRouteSpecification.class.st b/source/Stargate-REST-API/CorsAwareRouteSpecification.class.st new file mode 100644 index 0000000..90ec3e5 --- /dev/null +++ b/source/Stargate-REST-API/CorsAwareRouteSpecification.class.st @@ -0,0 +1,32 @@ +Class { + #name : #CorsAwareRouteSpecification, + #superclass : #Object, + #instVars : [ + 'specification' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +CorsAwareRouteSpecification class >> for: aRouteSpecification [ + + ^ self new initializeFor: aRouteSpecification +] + +{ #category : #visiting } +CorsAwareRouteSpecification >> accept: aRouteConfigurator [ + + aRouteConfigurator configureAsCorsAwareRoute: self +] + +{ #category : #initialization } +CorsAwareRouteSpecification >> initializeFor: aRouteSpecification [ + + specification := aRouteSpecification +] + +{ #category : #accessing } +CorsAwareRouteSpecification >> specification [ + + ^ specification +] diff --git a/source/Stargate-REST-API/CrossOriginResourceSharingHandler.class.st b/source/Stargate-REST-API/CrossOriginResourceSharingHandler.class.st new file mode 100644 index 0000000..a9b0344 --- /dev/null +++ b/source/Stargate-REST-API/CrossOriginResourceSharingHandler.class.st @@ -0,0 +1,43 @@ +Class { + #name : #CrossOriginResourceSharingHandler, + #superclass : #Object, + #instVars : [ + 'httpMethods' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +CrossOriginResourceSharingHandler class >> allowing: anHttpMethodsCollection [ + + ^ self new initializeAllowing: anHttpMethodsCollection +] + +{ #category : #'private - accessing' } +CrossOriginResourceSharingHandler >> commaSeparatedHttpMethods [ + + ^ (CollectionFormatter separatingWith: ', ') format: httpMethods +] + +{ #category : #initialization } +CrossOriginResourceSharingHandler >> initializeAllowing: anHttpMethodsCollection [ + + httpMethods := anHttpMethodsCollection +] + +{ #category : #evaluating } +CrossOriginResourceSharingHandler >> value: aRequest [ + + | response | + + response := ZnResponse noContent. + + response headers + at: 'Access-Control-Allow-Headers' + put: 'Access-Control-Allow-Origin, Content-Type, Accept'; + at: 'Access-Control-Allow-Methods' + put: self commaSeparatedHttpMethods; + at: 'Access-Control-Max-Age' put: '86400'. + + ^ response +] diff --git a/source/Stargate-REST-API/DecodingFailed.class.st b/source/Stargate-REST-API/DecodingFailed.class.st new file mode 100644 index 0000000..88305a8 --- /dev/null +++ b/source/Stargate-REST-API/DecodingFailed.class.st @@ -0,0 +1,5 @@ +Class { + #name : #DecodingFailed, + #superclass : #Error, + #category : #'Stargate-REST-API' +} diff --git a/source/Stargate-REST-API/DynamicDecoder.class.st b/source/Stargate-REST-API/DynamicDecoder.class.st new file mode 100644 index 0000000..a2e01d2 --- /dev/null +++ b/source/Stargate-REST-API/DynamicDecoder.class.st @@ -0,0 +1,47 @@ +Class { + #name : #DynamicDecoder, + #superclass : #Object, + #instVars : [ + 'key', + 'decoders' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +DynamicDecoder class >> determiningDecoderBy: aKey [ + + ^ self new initializeDeterminingDecoderBy: aKey + + +] + +{ #category : #configuring } +DynamicDecoder >> decoding: aKey applying: aBlock [ + + decoders at: aKey put: aBlock +] + +{ #category : #'initialize-release' } +DynamicDecoder >> initializeDeterminingDecoderBy: aKey [ + + key := aKey. + decoders := Dictionary new +] + +{ #category : #decoding } +DynamicDecoder >> value: aDictionary [ + + | criteria | + + criteria := aDictionary + at: key + ifAbsent: + [ DecodingFailed signal: ('Key <1s> not found' expandMacrosWith: key) ]. + + ^ decoders + at: criteria + ifPresent: [ :block | block value: aDictionary ] + ifAbsent: [ DecodingFailed + signal: ('Parser to parse <1s> not found' expandMacrosWith: key) ] +] diff --git a/source/Stargate-REST-API/HTTPClientError.class.st b/source/Stargate-REST-API/HTTPClientError.class.st new file mode 100644 index 0000000..bb05754 --- /dev/null +++ b/source/Stargate-REST-API/HTTPClientError.class.st @@ -0,0 +1,49 @@ +Class { + #name : #HTTPClientError, + #superclass : #Error, + #category : #'Stargate-REST-API' +} + +{ #category : #signaling } +HTTPClientError class >> signal: aCode describedBy: aFailureExplanation [ + + ^ self new + tag: aCode; + signal: aFailureExplanation +] + +{ #category : #signaling } +HTTPClientError class >> signalBadRequest: aFailureExplanation [ + + ^ self signal: 400 describedBy: aFailureExplanation +] + +{ #category : #signaling } +HTTPClientError class >> signalConflict: aFailureExplanation [ + + ^self signal: 409 describedBy: aFailureExplanation +] + +{ #category : #signaling } +HTTPClientError class >> signalNotFound [ + + ^self signal: 404 describedBy: 'Not found' +] + +{ #category : #signaling } +HTTPClientError class >> signalNotFound: aFailureExplanation [ + + ^self signal: 404 describedBy: aFailureExplanation +] + +{ #category : #signaling } +HTTPClientError class >> signalUnsupportedMediaType: aFailureExplanation [ + + ^ self signal: 415 describedBy: aFailureExplanation +] + +{ #category : #accessing } +HTTPClientError >> code [ + + ^self tag +] diff --git a/source/Stargate-REST-API/HttpRequestContext.class.st b/source/Stargate-REST-API/HttpRequestContext.class.st new file mode 100644 index 0000000..8df3219 --- /dev/null +++ b/source/Stargate-REST-API/HttpRequestContext.class.st @@ -0,0 +1,27 @@ +Class { + #name : #HttpRequestContext, + #superclass : #Object, + #instVars : [ + 'knownObjects' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #accessing } +HttpRequestContext >> hold: anObject under: aConcept [ + + knownObjects at: aConcept put: anObject +] + +{ #category : #initialization } +HttpRequestContext >> initialize [ + + super initialize. + knownObjects := Dictionary new +] + +{ #category : #accessing } +HttpRequestContext >> objectUnder: aConcept ifNone: aBlock [ + + ^ knownObjects at: aConcept ifAbsent: aBlock +] diff --git a/source/Stargate-REST-API/IsUUID.class.st b/source/Stargate-REST-API/IsUUID.class.st new file mode 100644 index 0000000..aca73e8 --- /dev/null +++ b/source/Stargate-REST-API/IsUUID.class.st @@ -0,0 +1,17 @@ +Class { + #name : #IsUUID, + #superclass : #IsObject, + #category : #'Stargate-REST-API' +} + +{ #category : #'type constraint' } +IsUUID class >> matchesTo: aString [ + + ^ aString isString and: [ '[\da-z]+' asRegex matches: aString ] +] + +{ #category : #'type constraint' } +IsUUID class >> parseString: aString [ + + ^ UUID fromString36: aString +] diff --git a/source/Stargate-REST-API/MappingNotFound.class.st b/source/Stargate-REST-API/MappingNotFound.class.st new file mode 100644 index 0000000..a9b092b --- /dev/null +++ b/source/Stargate-REST-API/MappingNotFound.class.st @@ -0,0 +1,5 @@ +Class { + #name : #MappingNotFound, + #superclass : #Error, + #category : #'Stargate-REST-API' +} diff --git a/source/Stargate-REST-API/MappingRule.class.st b/source/Stargate-REST-API/MappingRule.class.st new file mode 100644 index 0000000..f5a4066 --- /dev/null +++ b/source/Stargate-REST-API/MappingRule.class.st @@ -0,0 +1,72 @@ +Class { + #name : #MappingRule, + #superclass : #Object, + #instVars : [ + 'isDefault', + 'mediaType', + 'objectType', + 'mapper' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'private - instance creation' } +MappingRule class >> between: anObjectType and: aMediaType using: aMapper [ + + ^ self new + initializeBetween: anObjectType + and: aMediaType + using: aMapper +] + +{ #category : #'instance creation' } +MappingRule class >> decoding: aMediaType to: anObjectType using: aReader [ + + ^ self between: anObjectType and: aMediaType using: aReader +] + +{ #category : #'instance creation' } +MappingRule class >> encoding: anObjectType to: aMediaType using: aWriter [ + + ^ self between: anObjectType and: aMediaType using: aWriter +] + +{ #category : #applying } +MappingRule >> applyOn: anObjectToEncode within: aContext [ + + ^ mapper cull: anObjectToEncode cull: aContext +] + +{ #category : #configuring } +MappingRule >> beDefault [ + + isDefault := true +] + +{ #category : #initialization } +MappingRule >> initializeBetween: anObjectType and: aMediaType using: aMapper [ + + mapper := aMapper. + mediaType := aMediaType. + objectType := anObjectType. + + isDefault := false +] + +{ #category : #testing } +MappingRule >> isDefault [ + + ^ isDefault +] + +{ #category : #accessing } +MappingRule >> mediaType [ + + ^ mediaType +] + +{ #category : #accessing } +MappingRule >> objectType [ + + ^ objectType +] diff --git a/source/Stargate-REST-API/MappingRuleSet.class.st b/source/Stargate-REST-API/MappingRuleSet.class.st new file mode 100644 index 0000000..9063500 --- /dev/null +++ b/source/Stargate-REST-API/MappingRuleSet.class.st @@ -0,0 +1,126 @@ +Class { + #name : #MappingRuleSet, + #superclass : #Object, + #instVars : [ + 'decodingRules', + 'encodingRules' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +MappingRuleSet class >> consistingOf: encodingRules and: decodingRules [ + + ^ self new initializeConsistingOf: encodingRules and: decodingRules +] + +{ #category : #'private - querying' } +MappingRuleSet >> decodingRuleFor: anObjectType withMediaTypeEqualsTo: aMediaType ifNone: ifNoneBlock [ + + ^ self + ruleIn: decodingRules + for: anObjectType + withMediaTypeEqualsTo: aMediaType + ifNone: ifNoneBlock +] + +{ #category : #'private - querying' } +MappingRuleSet >> defaultDecodingRuleFor: anObjectType withMediaTypeMatching: aMediaType [ + + | defaultDecodingRule | + + defaultDecodingRule := self defaultRuleIn: decodingRules for: anObjectType. + + ^ ((self does: aMediaType match: defaultDecodingRule mediaType) + or: [ self isWildcard: aMediaType ]) + ifTrue: [ defaultDecodingRule ] + ifFalse: [ MappingNotFound signal: 'Decoder not found for given media type' ] +] + +{ #category : #'private - querying' } +MappingRuleSet >> defaultEncodingRuleFor: anObjectType withMediaTypeMatching: aMediaType [ + + | defaultEncodingRule | + + defaultEncodingRule := self defaultRuleIn: encodingRules for: anObjectType. + + ^ ((self does: aMediaType match: defaultEncodingRule mediaType) + or: [ self isWildcard: aMediaType ]) + ifTrue: [ defaultEncodingRule ] + ifFalse: [ MappingNotFound signal: 'Encoder not found for given media type' ] +] + +{ #category : #'private - querying' } +MappingRuleSet >> defaultRuleIn: mappingRules for: anObjectType [ + + ^ mappingRules + detect: + [ :decodingRule | decodingRule objectType = anObjectType and: [ decodingRule isDefault ] ] + ifNone: [ MappingNotFound signal ] +] + +{ #category : #'private - testing media type' } +MappingRuleSet >> does: aMediaType match: anotherMediaType [ + + ^ (aMediaType matches: anotherMediaType) + and: [ self isNotVersioned: aMediaType ] +] + +{ #category : #'private - querying' } +MappingRuleSet >> encodingRuleFor: anObjectType withMediaTypeEqualsTo: aMediaType ifNone: ifNoneBlock [ + + ^ self + ruleIn: encodingRules + for: anObjectType + withMediaTypeEqualsTo: aMediaType + ifNone: ifNoneBlock +] + +{ #category : #initialization } +MappingRuleSet >> initializeConsistingOf: aCollectionOfEncodingRules and: aCollectionOfDecodingRules [ + + encodingRules := aCollectionOfEncodingRules. + decodingRules := aCollectionOfDecodingRules +] + +{ #category : #'private - testing media type' } +MappingRuleSet >> isNotVersioned: aMediaType [ + + ^ (aMediaType parameters includesKey: 'version') not +] + +{ #category : #'private - testing media type' } +MappingRuleSet >> isWildcard: aMediaType [ + + ^ {aMediaType main. + aMediaType sub} allSatisfy: [ :part | part = '*' ] +] + +{ #category : #'private - querying' } +MappingRuleSet >> ruleIn: mappingRules for: anObjectType withMediaTypeEqualsTo: aMediaType ifNone: ifNoneBlock [ + + ^ mappingRules + detect: + [ :mappingRule | mappingRule mediaType = aMediaType and: [ mappingRule objectType = anObjectType ] ] + ifNone: ifNoneBlock +] + +{ #category : #querying } +MappingRuleSet >> ruleToDecode: aMediaType to: anObjectType [ + + ^ self + decodingRuleFor: anObjectType + withMediaTypeEqualsTo: aMediaType + ifNone: [ self defaultDecodingRuleFor: anObjectType withMediaTypeMatching: aMediaType ] +] + +{ #category : #querying } +MappingRuleSet >> ruleToEncode: anObjectType to: aMediaType [ + + ^ self + encodingRuleFor: anObjectType + withMediaTypeEqualsTo: aMediaType + ifNone: [ self + defaultEncodingRuleFor: anObjectType + withMediaTypeMatching: aMediaType ] +] diff --git a/source/Stargate-REST-API/MappingRuleSetBuilder.class.st b/source/Stargate-REST-API/MappingRuleSetBuilder.class.st new file mode 100644 index 0000000..c963b95 --- /dev/null +++ b/source/Stargate-REST-API/MappingRuleSetBuilder.class.st @@ -0,0 +1,148 @@ +Class { + #name : #MappingRuleSetBuilder, + #superclass : #Object, + #instVars : [ + 'decodingRules', + 'encodingRules' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #configuring } +MappingRuleSetBuilder >> addDefaultRuleToDecode: aMediaType to: aKeyRepresentingObjectType using: aReader [ + + | decodingRule | + + decodingRule := self + ruleToDecode: aMediaType + to: aKeyRepresentingObjectType + using: aReader. + + decodingRule beDefault. + decodingRules add: decodingRule +] + +{ #category : #configuring } +MappingRuleSetBuilder >> addDefaultRuleToEncode: aKeyRepresentingObjectType to: aMediaType using: aWriter [ + + | encodingRule | + + encodingRule := self + ruleToEncode: aMediaType + to: aKeyRepresentingObjectType + using: aWriter. + + encodingRule beDefault. + encodingRules add: encodingRule +] + +{ #category : #configuring } +MappingRuleSetBuilder >> addRuleToDecode: aMediaType to: aKeyRepresentingObjectType using: aReader [ + + | decodingRule | + + decodingRule := self + ruleToDecode: aMediaType + to: aKeyRepresentingObjectType + using: aReader. + + decodingRules add: decodingRule +] + +{ #category : #configuring } +MappingRuleSetBuilder >> addRuleToEncode: aKeyRepreseningObjectType to: aMediaType using: aWriter [ + + | encodingRule | + + encodingRule := self + ruleToEncode: aMediaType + to: aKeyRepreseningObjectType + using: aWriter. + + encodingRules add: encodingRule +] + +{ #category : #'private - preconditions' } +MappingRuleSetBuilder >> assertThereIsOnlyOneDefaultRuleForEachObjectType [ + + AssertionCheckerBuilder new + checking: [ :asserter | + asserter + enforce: [ (decodingRules groupedBy: #objectType) values + allSatisfy: + [ :groupedDecodingRules | (groupedDecodingRules count: [ :decodingRule | decodingRule isDefault ]) = 1 ] ] + because: 'You must provide a default decoder for each scope'; + enforce: [ (encodingRules groupedBy: #objectType) values + allSatisfy: + [ :groupedEncodingRules | (groupedEncodingRules count: [ :encodingRule | encodingRule isDefault ]) = 1 ] ] + because: 'You must provide a default decoder for each scope' ]; + buildAndCheck +] + +{ #category : #'private - preconditions' } +MappingRuleSetBuilder >> assertThereIsntConfiguredRuleToDecode: aMediaType to: anObjectType [ + + AssertionCheckerBuilder new + raising: ConflictingObjectFound; + checking: [ :asserter | + asserter + enforce: [ decodingRules + noneSatisfy: + [ :rule | rule mediaType = aMediaType and: [ rule objectType = anObjectType ] ] ] + because: 'Decoder for that MIME type already registered' ]; + buildAndCheck +] + +{ #category : #'private - preconditions' } +MappingRuleSetBuilder >> assertThereIsntConfiguredRuleToEncode: anObjectType to: aMediaType [ + + AssertionCheckerBuilder new + raising: ConflictingObjectFound; + checking: [ :asserter | + asserter + enforce: [ encodingRules + noneSatisfy: [ :rule | rule mediaType = aMediaType and: [ rule objectType = anObjectType ] ] ] + because: 'Encoder for that MIME type already registered' ]; + buildAndCheck +] + +{ #category : #building } +MappingRuleSetBuilder >> build [ + + self assertThereIsOnlyOneDefaultRuleForEachObjectType. + + ^ MappingRuleSet consistingOf: encodingRules and: decodingRules +] + +{ #category : #initialization } +MappingRuleSetBuilder >> initialize [ + + encodingRules := OrderedCollection new. + decodingRules := OrderedCollection new +] + +{ #category : #'private - configuring' } +MappingRuleSetBuilder >> ruleToDecode: aMediaType to: aKeyRepresentingObjectType using: aReader [ + + self + assertThereIsntConfiguredRuleToDecode: aMediaType + to: aKeyRepresentingObjectType. + + ^ MappingRule + decoding: aMediaType + to: aKeyRepresentingObjectType + using: aReader +] + +{ #category : #'private - configuring' } +MappingRuleSetBuilder >> ruleToEncode: aMediaType to: aKeyRepresentingObjectType using: aWriter [ + + self + assertThereIsntConfiguredRuleToEncode: aKeyRepresentingObjectType + to: aMediaType. + + ^ MappingRule + encoding: aKeyRepresentingObjectType + to: aMediaType + using: aWriter +] diff --git a/source/Stargate-REST-API/ReflectiveMappingRuleSetBuilder.class.st b/source/Stargate-REST-API/ReflectiveMappingRuleSetBuilder.class.st new file mode 100644 index 0000000..4ec195c --- /dev/null +++ b/source/Stargate-REST-API/ReflectiveMappingRuleSetBuilder.class.st @@ -0,0 +1,36 @@ +Class { + #name : #ReflectiveMappingRuleSetBuilder, + #superclass : #Object, + #instVars : [ + 'specification' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +ReflectiveMappingRuleSetBuilder class >> for: aWebServiceSpecification [ + + ^ self new initializeFor: aWebServiceSpecification +] + +{ #category : #building } +ReflectiveMappingRuleSetBuilder >> build [ + + | builder | + + builder := MappingRuleSetBuilder new. + + (KeywordMessageSendingCollector + sendingAllMessagesBeginningWith: 'add' + andEndingWith: 'MappingIn:' + to: specification + with: builder) value. + + ^ builder build +] + +{ #category : #initialization } +ReflectiveMappingRuleSetBuilder >> initializeFor: aWebServiceSpecification [ + + specification := aWebServiceSpecification +] diff --git a/source/Stargate-REST-API/ReflectiveRoutesConfigurator.class.st b/source/Stargate-REST-API/ReflectiveRoutesConfigurator.class.st new file mode 100644 index 0000000..4514575 --- /dev/null +++ b/source/Stargate-REST-API/ReflectiveRoutesConfigurator.class.st @@ -0,0 +1,43 @@ +Class { + #name : #ReflectiveRoutesConfigurator, + #superclass : #Object, + #instVars : [ + 'teapot' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +ReflectiveRoutesConfigurator class >> appliedTo: aTeapot [ + + ^ self new initializeAppliedTo: aTeapot +] + +{ #category : #configuring } +ReflectiveRoutesConfigurator >> addRoutesOf: aWebService [ + + | routeConfigurator | + + routeConfigurator := RouteConfigurator + appliedTo: teapot + sendingMessagesTo: aWebService. + + (self specifiedRoutesFor: aWebService) + do: [ :routeSpecification | routeSpecification accept: routeConfigurator ]. + + routeConfigurator configureCrossOriginSharingRoutes +] + +{ #category : #initialization } +ReflectiveRoutesConfigurator >> initializeAppliedTo: aTeapot [ + + teapot := aTeapot +] + +{ #category : #accessing } +ReflectiveRoutesConfigurator >> specifiedRoutesFor: aWebService [ + + ^ (UnaryMessageSendingCollector + sendingAllMessagesEndingWith: 'Route' + to: aWebService specification) value +] diff --git a/source/Stargate-REST-API/RouteConfigurator.class.st b/source/Stargate-REST-API/RouteConfigurator.class.st new file mode 100644 index 0000000..07e8981 --- /dev/null +++ b/source/Stargate-REST-API/RouteConfigurator.class.st @@ -0,0 +1,65 @@ +Class { + #name : #RouteConfigurator, + #superclass : #Object, + #instVars : [ + 'teapot', + 'webService', + 'routesAllowingCors' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +RouteConfigurator class >> appliedTo: aTeapot sendingMessagesTo: aWebService [ + + ^ self new initializeAppliedTo: aTeapot sendingMessagesTo: aWebService +] + +{ #category : #'private - configuring' } +RouteConfigurator >> configureAsCorsAwareRoute: aCorsAwareRouteSpecification [ + + | specification | + + specification := aCorsAwareRouteSpecification specification. + + routesAllowingCors + at: specification resourceLocation + ifPresent: [ :httpMethods | httpMethods add: specification httpMethod ] + ifAbsentPut: (OrderedCollection with: specification httpMethod). + + specification accept: self +] + +{ #category : #configuring } +RouteConfigurator >> configureCrossOriginSharingRoutes [ + + routesAllowingCors keys + do: [ :resourceLocation | + teapot + OPTIONS: + resourceLocation + -> (CrossOriginResourceSharingHandler allowing: (routesAllowingCors at: resourceLocation)) ] +] + +{ #category : #'private - configuring' } +RouteConfigurator >> configureRoute: aRouteSpecification [ + + teapot + perform: (aRouteSpecification httpMethod , ':') asSymbol + with: + aRouteSpecification resourceLocation + -> [ :request | + webService + perform: aRouteSpecification message + with: request + with: HttpRequestContext new ] +] + +{ #category : #initialization } +RouteConfigurator >> initializeAppliedTo: aTeapot sendingMessagesTo: aWebService [ + + teapot := aTeapot . + webService := aWebService. + + routesAllowingCors := Dictionary new. +] diff --git a/source/Stargate-REST-API/RouteSpecification.class.st b/source/Stargate-REST-API/RouteSpecification.class.st new file mode 100644 index 0000000..342e373 --- /dev/null +++ b/source/Stargate-REST-API/RouteSpecification.class.st @@ -0,0 +1,54 @@ +Class { + #name : #RouteSpecification, + #superclass : #Object, + #instVars : [ + 'httpMethod', + 'resourceLocation', + 'message' + ], + #category : #'Stargate-REST-API' +} + +{ #category : #'instance creation' } +RouteSpecification class >> handling: anHttpMethod at: aResourceLocation sending: aMessage [ + + ^ self new initializeHandling: anHttpMethod at: aResourceLocation sending: aMessage +] + +{ #category : #visiting } +RouteSpecification >> accept: aRouteConfigurator [ + + aRouteConfigurator configureRoute: self +] + +{ #category : #decorating } +RouteSpecification >> asCorsAware [ + + ^ CorsAwareRouteSpecification for: self +] + +{ #category : #accessing } +RouteSpecification >> httpMethod [ + + ^ httpMethod +] + +{ #category : #initialization } +RouteSpecification >> initializeHandling: anHttpMethod at: aResourceLocation sending: aMessage [ + + httpMethod := anHttpMethod. + resourceLocation := aResourceLocation. + message := aMessage +] + +{ #category : #accessing } +RouteSpecification >> message [ + + ^ message +] + +{ #category : #accessing } +RouteSpecification >> resourceLocation [ + + ^ resourceLocation +] diff --git a/source/Stargate-REST-API/UUID.extension.st b/source/Stargate-REST-API/UUID.extension.st new file mode 100644 index 0000000..66108b8 --- /dev/null +++ b/source/Stargate-REST-API/UUID.extension.st @@ -0,0 +1,7 @@ +Extension { #name : #UUID } + +{ #category : #'*Stargate-REST-API' } +UUID >> neoJsonOn: neoJSONWriter [ + + neoJSONWriter writeString: self asString36 +] diff --git a/source/Stargate-REST-API/WebServiceSpecification.class.st b/source/Stargate-REST-API/WebServiceSpecification.class.st new file mode 100644 index 0000000..b1e133d --- /dev/null +++ b/source/Stargate-REST-API/WebServiceSpecification.class.st @@ -0,0 +1,5 @@ +Class { + #name : #WebServiceSpecification, + #superclass : #Object, + #category : #'Stargate-REST-API' +} diff --git a/source/Stargate-REST-API/ZnEntity.extension.st b/source/Stargate-REST-API/ZnEntity.extension.st new file mode 100644 index 0000000..e3218f7 --- /dev/null +++ b/source/Stargate-REST-API/ZnEntity.extension.st @@ -0,0 +1,7 @@ +Extension { #name : #ZnEntity } + +{ #category : #'*Stargate-REST-API' } +ZnEntity class >> json: json [ + + ^ self stringEntityClass json: json +] diff --git a/source/Stargate-REST-API/ZnStringEntity.extension.st b/source/Stargate-REST-API/ZnStringEntity.extension.st new file mode 100644 index 0000000..9e0b6cf --- /dev/null +++ b/source/Stargate-REST-API/ZnStringEntity.extension.st @@ -0,0 +1,9 @@ +Extension { #name : #ZnStringEntity } + +{ #category : #'*Stargate-REST-API' } +ZnStringEntity class >> json: string [ + + ^ (self type: ZnMimeType applicationJson) + string: string; + yourself +] diff --git a/source/Stargate-REST-API/package.st b/source/Stargate-REST-API/package.st new file mode 100644 index 0000000..1b4d1c8 --- /dev/null +++ b/source/Stargate-REST-API/package.st @@ -0,0 +1 @@ +Package { #name : #'Stargate-REST-API' }