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' }