Updated, improved and cleaned RESTbucks example.

Moved it under "rest" subfolder.
This commit is contained in:
2017-02-13 16:23:38 +01:00
parent a44c4d9a16
commit b56aec67a9
35 changed files with 1826 additions and 1419 deletions

View File

@@ -7,13 +7,13 @@
<exclude>/.svn$</exclude>
<exclude>/EIFGENs$</exclude>
</file_rule>
<option warning="true" void_safety="none">
<option warning="true" void_safety="all">
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
<library name="http_client" location="..\..\..\library\network\http_client\http_client.ecf"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="http_client" location="..\..\..\..\library\network\http_client\http_client-safe.ecf"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<cluster name="src" location=".\src\" recursive="true"/>
</target>
</system>

View File

@@ -0,0 +1,177 @@
note
description : "Objects that ..."
author : "$Author$"
date : "$Date$"
revision : "$Revision$"
class
RESTBUCK_CLIENT
create
make
feature {NONE} -- Initialization
make
-- Initialize `Current'.
local
h: NET_HTTP_CLIENT
sess: HTTP_CLIENT_SESSION
resp : detachable HTTP_CLIENT_RESPONSE
l_location : detachable READABLE_STRING_8
body : STRING
do
create h
sess := h.new_session ("http://127.0.0.1:" + server_port.out)
-- Uncomment the following 2 lines, if you use fiddler2 web debugging tool
-- sess.set_is_debug (True)
-- sess.set_proxy ("127.0.0.1", 8888)
-- Create Order
print ("> Create Order %N")
resp := create_order (sess)
display_response (resp)
-- Read the Order
l_location := resp.header ("Location")
if l_location /= Void then
print ("> Read Order from " + l_location + " %N")
resp := read_order (sess, l_location)
display_response (resp)
else
print ("> Previous order creation failed, no location returned!%N")
end
-- Update the Order
if resp /= Void and then attached resp.body as l_body then
body := l_body.as_string_8
body.replace_substring_all ("takeAway", "in Shop")
print ("> Update Order: change location from [takeAway] to [in Shop] %N")
resp := update_order (sess, l_location, body)
display_response (resp)
end
-- Pay the Order
if resp /= Void and then attached resp.body as l_body then
body := l_body.as_string_8
body.replace_substring_all ("%"status%":%"submitted%"", "%"status%":%"paid%"")
print ("> Pay Order: should trigger validation error!%N")
resp := update_order (sess, l_location, body)
display_response (resp)
end
end
update_order (sess: HTTP_CLIENT_SESSION; uri: detachable READABLE_STRING_8; a_body: STRING): detachable HTTP_CLIENT_RESPONSE
local
context : HTTP_CLIENT_REQUEST_CONTEXT
do
if uri /= Void then
sess.set_base_url (uri)
create context.make
context.headers.put ("application/json", "Content-Type")
Result := sess.put ("", context, a_body )
end
end
read_order (sess: HTTP_CLIENT_SESSION; uri: detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
do
if uri /= Void then
sess.set_base_url (uri)
Result := sess.get ("", Void)
end
end
create_order (sess: HTTP_CLIENT_SESSION) : HTTP_CLIENT_RESPONSE
local
s: READABLE_STRING_8
j: JSON_PARSER
context : HTTP_CLIENT_REQUEST_CONTEXT
do
s := "[
{
"location":"takeAway",
"items":[
{
"name":"Late",
"option":"skim",
"size":"Small",
"quantity":1
}
]
}
]"
create context.make
context.headers.put ("application/json", "Content-Type")
Result := sess.post ("/order", context, s)
end
display_response (resp: detachable HTTP_CLIENT_RESPONSE)
do
io.error.put_string (create {STRING}.make_filled ('-', 40))
io.error.put_new_line
if resp = Void then
io.error.put_string ("ERROR: No response~%N")
else
if resp.error_occurred and attached resp.error_message as err_msg then
io.error.put_string ("[ERROR] ")
io.error.put_string (err_msg)
io.error.put_new_line
end
-- Display response status and header
io.error.put_string ("Status code: " + resp.status.out + "%N")
across
resp.headers as l_headers
loop
io.error.put_string (l_headers.item.name)
io.error.put_string (":")
io.error.put_string (l_headers.item.value)
io.error.put_new_line
end
-- Show the Response body
if not resp.error_occurred and attached resp.body as m then
io.error.put_string (m)
io.error.put_new_line
end
end
io.error.put_string (create {STRING}.make_filled ('-', 40))
io.error.put_new_line
end
feature {NONE} -- Implementation
server_port: INTEGER
local
f: PLAIN_TEXT_FILE
p: PATH
s: STRING
do
create p.make_current
p := p.extended ("..").extended ("server.ini")
create f.make_with_path (p)
if f.exists and then f.is_access_readable then
f.open_read
from
until
f.exhausted or f.end_of_file or Result > 0
loop
f.read_line
s := f.last_string
if s.starts_with_general ("port=") then
s.remove_head (5)
if s.is_integer then
Result := s.to_integer
end
end
end
f.close
end
if Result <= 0 then
Result := 80
end
end
end

View File

@@ -6,100 +6,68 @@ the HTTP protocol as an application protocol instead of a transport protocol.
Restbuck Protocol
-----------------
<table>
<TR><TH>Verb</TH> <TH>URI or template</TH> <TH>Use</TH></TR>
<TR><TD>POST</TD> <TD>/order</TD> <TD>Create a new order, and upon success, receive a Locationheader specifying the new order's URI.</TD></TR>
<TR><TD>GET</TD> <TD>/order/{orderId}</TD> <TD>Request the current state of the order specified by the URI.</TD></TR>
<TR><TD>PUT</TD> <TD>/order/{orderId}</TD> <TD>Update an order at the given URI with new information, providing the full representation.</TD></TR>
<TR><TD>DELETE</TD> <TD>/order/{orderId}</TD> <TD>Logically remove the order identified by the given URI.</TD></TR>
</table>
* Method `POST` with URI `/order` : Create a new order, and upon success, receive a Locationheader specifying the new order's URI.
* Method `GET` with URI-template `/order/{orderId}` : Request the current state of the order specified by the URI.
* Method `PUT` with URI-template `/order/{orderId}` : Update an order at the given URI with new information, providing the full representation.
* Method `DELETE` with URI-tempalte `/order/{orderId}` : Logically remove the order identified by the given URI.
Resource Represenation
----------------------
The previous tables shows a contrat, the URI or URI template, allows us to indentify resources, now we will chose a
representacion, for this particular case we will use JSON.
The previous tables shows a contrat, the URI or URI-template, allows us to indentify resources, now we will chose a
representation, for this particular case we will use JSON.
Note: <br/>
1. *A resource can have multiple URIs*.<br/>
2. *A resource can have multiple Representations*.<br/>
Note:
1. *A resource can have multiple URIs*.
2. *A resource can have multiple Representations*.
RESTBUCKS_SERVER
----------------
This class implement the main entry of our REST CRUD service, we are using a default connector (Standalone Connector,
using a WebServer written in Eiffel).
We are inheriting from URI_TEMPLATE_ROUTED_SERVICE, this allows us to map our service contrat, as is shown in the previous
table, the mapping is defined in the feature setup_router, this also show that the class ORDER_HANDLER will be encharge
of to handle different type of request to the ORDER resource.
We are inheriting from `WSF_ROUTED_SKELETON_EXECUTION`, this allows us to map our service contrat, as is shown in the previous
table, the mapping is defined in the feature `setup_router`, this also show that the class `ORDER_HANDLER` will be in charge
of handling different types of request to the ORDER resource.
```
class RESTBUCKS_SERVER_EXECUTION
class
RESTBUCKS_SERVER
inherit
ANY
URI_TEMPLATE_ROUTED_SERVICE
DEFAULT_SERVICE
-- Here we are using a default connector using the default Standalone Connector,
-- but it's possible to use other connector (CGI or FCGI).
WSF_ROUTED_SKELETON_EXECUTION
undefine
requires_proxy
end
WSF_NO_PROXY_POLICY
SHARED_RESTBUCKS_API
create
make
feature {NONE} -- Initialization
make
-- Initialize the router (this will have the request handler and
-- their context).
do
initialize_router
make_and_launch
end
create_router
do
create router.make (2)
end
setup_router
local
order_handler: ORDER_HANDLER [REQUEST_URI_TEMPLATE_HANDLER_CONTEXT]
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
do
create order_handler
router.map_with_request_methods ("/order", order_handler, <<"POST">>)
router.map_with_request_methods ("/order/{orderid}", order_handler, <<"GET", "DELETE", "PUT">>)
setup_order_handler (router)
create doc.make_hidden (router)
router.handle ("/api/doc", doc, router.methods_GET)
end
feature -- Execution
execute_default (req: WSF_REQUEST; res: WSF_RESPONSE)
-- I'm using this method to handle the method not allowed response
-- in the case that the given uri does not have a corresponding http method
-- to handle it.
setup_order_handler (a_router: WSF_ROUTER)
local
h : HTTP_HEADER
l_description : STRING
l_api_doc : STRING
order_handler: ORDER_HANDLER
do
if req.content_length_value > 0 then
req.input.read_string (req.content_length_value.as_integer_32)
end
create h.make
h.put_status ({HTTP_STATUS_CODE}.method_not_allowed)
h.put_content_type_text_plain
l_api_doc := "%NPlease check the API%NURI:/order METHOD: POST%NURI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
l_description := req.request_method + req.request_uri + " is not allowed" + "%N" + l_api_doc
h.put_content_length (l_description.count)
h.put_current_date
res.set_status_code ({HTTP_STATUS_CODE}.method_not_allowed)
res.write_header_text (h.string)
res.write_string (l_description)
create order_handler.make ("orderid", a_router)
router.handle ("/order", order_handler, router.methods_POST)
router.handle ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
end
end
```
How to Create an order with POST
--------------------------------
@@ -111,7 +79,7 @@ If the request POST is SUCCESS, the server will create the order and will respon
if the request POST is not SUCCESS, the server will response with
400 BAD REQUEST, the client send a bad request or
500 INTERNAL_SERVER_ERROR, when the server can deliver the request.
```
POST /order HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
@@ -134,9 +102,10 @@ if the request POST is not SUCCESS, the server will response with
}
]
}
```
Response success
```
HTTP/1.1 201 Created
Status 201 Created
Content-Type application/json
@@ -154,6 +123,7 @@ Response success
"option" : "skim"
} ]
}
```
note:
curl -vv http://localhost:9090/order -H "Content-Type: application/json" -d "{\"location\":\"takeAway\",\"items\":[{\"name\":\"Late\",\"option\":\"skim\",\"size\":\"Small\",\"quantity\":1}]}" -X POST
@@ -166,6 +136,7 @@ If the GET request is SUCCESS, we response with 200 OK, and a representation of
If the GET request is not SUCCESS, we response with 404 Resource not found
If is a Conditional GET and the resource does not change we send a 304, Resource not modifed
```
GET /order/1 HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
@@ -174,8 +145,10 @@ If is a Conditional GET and the resource does not change we send a 304, Resource
Accept-Language: es-419,es;q=0.8,en;q=0.6
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
If-None-Match: 6542EF270D91D3EAF39CFB382E4CEBA7
```
Response
```
HTTP/1.1 200 OK
Status 200 OK
@@ -194,6 +167,7 @@ Response
"option" : "skim"
} ]
}
```
note:
curl -vv http://localhost:9090/order/1
@@ -210,8 +184,7 @@ If the request is a Conditional PUT, and it does not mat we response 415, precon
Suposse that we had created an Order with the values shown in the _How to create an order with POST_
But we change our decision and we want to stay in the shop.
```
PUT /order/1 HTTP/1.1
Content-Length: 122
Content-Type: application/json; charset=UTF-8
@@ -229,10 +202,10 @@ But we change our decision and we want to stay in the shop.
"option" : "skim"
} ]
}
```
Response success
```
HTTP/1.1 200 OK
Status 200 OK
Content-Type application/json
@@ -250,6 +223,7 @@ Response success
"option" : "skim"
} ]
}
```
How to Delete an order with DELETE
----------------------------------
@@ -259,28 +233,34 @@ Here we use DELETE to cancel an order, if that order is in state where it can st
405 if consumer and service's view of the resouce state is inconsisent
500 if we have an internal server error
```
DELETE /order/1 HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
```
Response success
```
HTTP/1.1 204 No Content
Status 204 No Content
Content-Type application/json
Date FRI,09 DEC 2011 21:10:51.00 GMT
```
If we want to check that the resource does not exist anymore we can try to retrieve a GET /order/1 and we will receive a
404 No Found
```
GET /order/1 HTTP/1.1
Host: localhost:8080
Connection: Keep-Alive
```
Response
```
HTTP/1.1 404 Not Found
Status 404 Not Found
@@ -289,9 +269,10 @@ Response
Date FRI,09 DEC 2011 21:14:17.79 GMT
The following resource/order/1 is not found
```
References
----------
1. [How to get a cup of coffe](http://www.infoq.com/articles/webber-rest-workflow)
2. [Rest in Practice] (http://restinpractice.com/default.aspx)
2. [Rest in Practice](http://restinpractice.com/default.aspx)

View File

@@ -9,19 +9,19 @@
<option void_safety="all">
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="none"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="conneg" location="..\..\library\network\protocol\content_negotiation\conneg-safe.ecf"/>
<library name="conneg" location="..\..\..\library\network\protocol\content_negotiation\conneg-safe.ecf"/>
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf" readonly="false"/>
<library name="default_standalone" location="..\..\library\server\wsf\default\standalone-safe.ecf" readonly="false"/>
<library name="encoder" location="..\..\library\text\encoder\encoder-safe.ecf" readonly="false"/>
<library name="http" location="..\..\library\network\protocol\http\http-safe.ecf" readonly="false"/>
<library name="default_standalone" location="..\..\..\library\server\wsf\default\standalone-safe.ecf" readonly="false"/>
<library name="encoder" location="..\..\..\library\text\encoder\encoder-safe.ecf" readonly="false"/>
<library name="http" location="..\..\..\library\network\protocol\http\http-safe.ecf" readonly="false"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
<library name="uri_template" location="..\..\library\text\parser\uri_template\uri_template-safe.ecf" readonly="false"/>
<library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/>
<library name="wsf_extension" location="..\..\library\server\wsf\wsf_extension-safe.ecf" readonly="false"/>
<library name="uri_template" location="..\..\..\library\text\parser\uri_template\uri_template-safe.ecf" readonly="false"/>
<library name="wsf" location="..\..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/>
<library name="wsf_extension" location="..\..\..\library\server\wsf\wsf_extension-safe.ecf" readonly="false"/>
</target>
<target name="restbucks" extends="restbucks_common">
<root class="RESTBUCKS_SERVER" feature="make"/>
@@ -41,10 +41,10 @@
<debug name="standalone" enabled="true"/>
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
</option>
<library name="wsf_policy_driven" location="..\..\library\server\wsf\wsf_policy_driven-safe.ecf" readonly="false"/>
<library name="wsf_policy_driven" location="..\..\..\library\server\wsf\wsf_policy_driven-safe.ecf" readonly="false"/>
<cluster name="src" location="src\" recursive="true">
<file_rule>
<exclude>/resource$</exclude>
<exclude>/resource/order_handler.e$</exclude>
</file_rule>
</cluster>
</target>

View File

@@ -0,0 +1,167 @@
note
description: "Summary description for {ORDER_JSON_SERIALIZATION}."
date: "$Date$"
revision: "$Revision$"
class
ORDER_JSON_SERIALIZATION
inherit
JSON_SERIALIZER
JSON_DESERIALIZER
feature -- Conversion
to_json (obj: detachable ANY; ctx: JSON_SERIALIZER_CONTEXT): JSON_VALUE
-- JSON value representing the JSON serialization of Eiffel value `obj', in the eventual context `ctx'.
local
j_order: JSON_OBJECT
j_item: JSON_OBJECT
ja: JSON_ARRAY
do
has_error := False
if attached {ORDER} obj as l_order then
create j_order.make_with_capacity (4)
Result := j_order
j_order.put_string (l_order.id, id_key)
if attached l_order.location as loc then
j_order.put_string (loc, location_key)
end
j_order.put_string (l_order.status, status_key)
if attached l_order.items as l_items and then not l_items.is_empty then
create ja.make (l_items.count)
j_order.put (ja, items_key)
across
l_items as ic
loop
if attached {ORDER_ITEM} ic.item as l_item then
create j_item.make_with_capacity (4)
j_item.put_string (l_item.name, name_key)
j_item.put_string (l_item.size, size_key)
j_item.put_integer (l_item.quantity, quantity_key)
j_item.put_string (l_item.option, option_key)
ja.extend (j_item)
end
end
end
else
create {JSON_NULL} Result
has_error := True
end
end
from_json (a_json: detachable JSON_VALUE; ctx: JSON_DESERIALIZER_CONTEXT; a_type: detachable TYPE [detachable ANY]): detachable ORDER
-- <Precursor/>.
local
l_status: detachable STRING_32
q: NATURAL_8
is_valid_from_json: BOOLEAN
l_name, l_size, l_option: detachable READABLE_STRING_32
do
has_error := False
is_valid_from_json := True
if attached {JSON_OBJECT} a_json as jobj then
-- Either new order (i.e no id and no status)
-- or an existing order with `id` and `status` (could be Void, thus use default).
if attached {JSON_STRING} jobj.item (status_key) as j_status then
l_status := j_status.unescaped_string_32
end
if
attached {JSON_STRING} jobj.item (id_key) as j_id
then
-- Note: the id has to be valid string 8 value!
create Result.make (j_id.unescaped_string_8, l_status)
elseif attached {JSON_NUMBER} jobj.item (id_key) as j_id then
-- Be flexible and accept json number as id.
create Result.make (j_id.integer_64_item.out, l_status)
else
create Result.make_empty
if l_status /= Void then
Result.set_status (l_status)
end
end
if attached {JSON_STRING} jobj.item (location_key) as j_location then
Result.set_location (j_location.unescaped_string_32)
end
if attached {JSON_ARRAY} jobj.item (items_key) as j_items then
across
j_items as ic
loop
if attached {JSON_OBJECT} ic.item as j_item then
if
attached {JSON_NUMBER} j_item.item (quantity_key) as j_quantity and then
j_quantity.integer_64_item < {NATURAL_8}.Max_value
then
q := j_quantity.integer_64_item.to_natural_8
else
q := 0
end
if
attached {JSON_STRING} j_item.item (name_key) as j_name and then
attached {JSON_STRING} j_item.item (size_key) as j_size and then
attached {JSON_STRING} j_item.item (option_key) as j_option
then
l_name := j_name.unescaped_string_32
l_size := j_size.unescaped_string_32
l_option := j_option.unescaped_string_32
if is_valid_item_customization (l_name, l_size, l_option, q) then
Result.add_item (create {ORDER_ITEM}.make (l_name, l_size, l_option, q))
else
is_valid_from_json := False
end
else
is_valid_from_json := False
end
end
end
end
if not is_valid_from_json or Result.items.is_empty then
Result := Void
end
else
is_valid_from_json := a_json = Void or else attached {JSON_NULL} a_json
Result := Void
end
has_error := not is_valid_from_json
end
has_error: BOOLEAN
-- Error occurred during last `from_json` or `to_json` execution.
feature {NONE} -- Implementation
id_key: STRING = "id"
location_key: STRING = "location"
status_key: STRING = "status"
items_key: STRING = "items"
name_key: STRING = "name"
size_key: STRING = "size"
quantity_key: STRING = "quantity"
option_key: STRING = "option"
feature -- Validation
is_valid_item_customization (name: READABLE_STRING_GENERAL; size: READABLE_STRING_GENERAL; option: READABLE_STRING_GENERAL; quantity: NATURAL_8): BOOLEAN
local
ic: ORDER_ITEM_VALIDATION
do
create ic
Result := ic.is_valid_coffee_type (name) and
ic.is_valid_milk_type (option) and
ic.is_valid_size_option (size) and
quantity > 0
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,44 @@
note
description: "[
Basic database for simple example.
(no concurrency access control, ...)
]"
date: "$Date$"
revision: "$Revision$"
deferred class
BASIC_DATABASE
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
deferred
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
deferred
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
deferred
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
deferred
ensure
has_id: cl_entry_id.item /= Void
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
require
has_item: has (a_entry_type, a_id)
deferred
ensure
has_not_item: not has (a_entry_type, a_id)
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,177 @@
note
description: "[
Basic database for simple example using JSON files.
(no concurrency access control, ...)
]"
date: "$Date$"
revision: "$Revision$"
class
BASIC_JSON_FS_DATABASE
inherit
BASIC_DATABASE
create
make
feature {NONE} -- Initialization
make (a_location: PATH)
local
d: DIRECTORY
do
location := a_location
create serialization
ensure_directory_exists (a_location)
end
feature -- Access serialization
serialization: JSON_SERIALIZATION
feature -- Access
location: PATH
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
local
d: DIRECTORY
do
create d.make_with_path (location.extended (entry_type_name (a_entry_type)))
if d.exists then
across
d.entries as ic
loop
if attached ic.item.extension as e and then e.is_case_insensitive_equal ("json") then
Result := Result + 1
end
end
end
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
local
fut: FILE_UTILITIES
do
Result := fut.file_path_exists (entry_path (a_entry_type, a_id))
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
local
f: RAW_FILE
s: STRING
do
create f.make_with_path (entry_path (a_entry_type, a_id))
if f.exists then
create s.make (f.count)
f.open_read
from
until
f.exhausted or f.end_of_file
loop
f.read_stream (1_024)
s.append (f.last_string)
end
f.close
Result := serialization.from_json_string (s, a_entry_type)
end
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
local
f: RAW_FILE
l_id: detachable READABLE_STRING_GENERAL
do
l_id := cl_entry_id.item
if l_id = Void then
l_id := next_identifier (a_entry_type)
cl_entry_id.replace (l_id)
end
create f.make_with_path (entry_path (a_entry_type, l_id))
ensure_directory_exists (f.path.parent)
f.open_write
f.put_string (serialization.to_json (a_entry).representation)
f.close
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
local
f: RAW_FILE
do
create f.make_with_path (entry_path (a_entry_type, a_id))
if f.exists and then f.is_access_writable then
f.delete
end
end
feature {NONE} -- Implementation
ensure_directory_exists (dn: PATH)
local
d: DIRECTORY
do
create d.make_with_path (dn)
if not d.exists then
d.recursive_create_dir
end
end
entry_path (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): PATH
do
Result := location.extended (entry_type_name (a_entry_type)).extended (a_id).appended_with_extension ("json")
end
entry_type_name (a_entry_type: TYPE [detachable ANY]): STRING
do
Result := a_entry_type.name.as_lower
Result.prune_all ('!')
end
last_id_file_path (a_entry_type: TYPE [detachable ANY]): PATH
do
Result := location.extended (entry_type_name (a_entry_type)).extended ("last-id")
end
next_identifier (a_entry_type: TYPE [detachable ANY]): STRING_8
local
i: NATURAL_64
f: RAW_FILE
s: STRING
do
create f.make_with_path (last_id_file_path (a_entry_type))
ensure_directory_exists (f.path.parent)
if f.exists then
create s.make (f.count)
f.open_read
f.read_line
s := f.last_string
f.close
if s.is_natural_64 then
i := s.to_natural_64
end
end
from
i := i + 1
Result := i.out
until
not has (a_entry_type, Result)
loop
i := i + 1
Result := i.out
end
f.open_write
f.put_string (Result)
f.close
end
invariant
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,102 @@
note
description: "[
Basic database for simple example based on memory.
]"
date: "$Date$"
revision: "$Revision$"
class
BASIC_MEMORY_DATABASE
inherit
BASIC_DATABASE
create
make
feature {NONE} -- Initialization
make
do
create collections.make (0)
end
collections: HASH_TABLE [STRING_TABLE [detachable ANY], TYPE [detachable ANY]]
feature -- Access
count_of (a_entry_type: TYPE [detachable ANY]): INTEGER
do
if attached collections.item (a_entry_type) as tb then
Result := tb.count
end
end
has (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): BOOLEAN
-- Has entry of type `a_entry_type` associated with id `a_id`?
do
if attached collections.item (a_entry_type) as tb then
Result := tb.has_key (a_id)
end
end
item (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL): detachable ANY
do
if attached collections.item (a_entry_type) as tb then
Result := tb.item (a_id)
end
end
save (a_entry_type: TYPE [detachable ANY]; a_entry: detachable ANY; cl_entry_id: CELL [detachable READABLE_STRING_GENERAL])
local
tb: detachable STRING_TABLE [detachable ANY]
l_id: detachable READABLE_STRING_GENERAL
do
tb := collections.item (a_entry_type)
if tb = Void then
create tb.make (100)
collections.force (tb, a_entry_type)
end
l_id := cl_entry_id.item
if l_id = Void then
l_id := next_identifier (a_entry_type)
cl_entry_id.replace (l_id)
end
tb.force (a_entry, l_id)
end
delete (a_entry_type: TYPE [detachable ANY]; a_id: READABLE_STRING_GENERAL)
do
if attached collections.item (a_entry_type) as tb then
tb.remove (a_id)
end
end
feature {NONE} -- Implementation
next_identifier (a_entry_type: TYPE [detachable ANY]): STRING_8
local
i: INTEGER
f: RAW_FILE
s: STRING
do
if attached collections.item (a_entry_type) as tb then
i := tb.count
else
i := 0
end
from
i := i + 1
Result := i.out
until
not has (a_entry_type, Result)
loop
i := i + 1
Result := i.out
end
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,173 @@
note
description: "Summary description for {RESTBUCKS_API}."
date: "$Date$"
revision: "$Revision$"
class
RESTBUCKS_API
create
make
feature {NONE} -- Initialization
make
local
db: BASIC_JSON_FS_DATABASE
do
create db.make (database_path)
db.serialization.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
database := db
end
feature -- Access
orders_count: INTEGER
-- Number of existing orders.
do
Result := database.count_of ({ORDER})
end
has_order (a_id: READABLE_STRING_GENERAL): BOOLEAN
do
Result := database.has ({ORDER}, a_id)
end
order (a_id: READABLE_STRING_GENERAL): detachable ORDER
do
if attached {ORDER} database.item ({ORDER}, a_id) as o then
Result := o
end
end
feature -- Element change
submit_order (a_order: ORDER)
-- Submit new order `a_order`.
require
no_id: not a_order.has_id
do
a_order.mark_submitted
save_order (a_order)
end
save_order (a_order: ORDER)
local
cl: CELL [detachable READABLE_STRING_GENERAL]
do
a_order.add_revision
if a_order.has_id then
create cl.put (a_order.id)
else
create cl.put (Void)
end
database.save ({ORDER}, a_order, cl)
if attached cl.item as l_new_id then
if l_new_id.is_valid_as_string_8 then
a_order.set_id (l_new_id.to_string_8)
else
check valid_id: False end
end
end
ensure
has_id: a_order.has_id
incremented_revision: a_order.revision > old (a_order.revision)
end
delete_order (a_order: ORDER)
do
database.delete ({ORDER}, a_order.id)
end
feature -- Access: order status
is_valid_status_state (a_status: STRING): BOOLEAN
-- Is `a_status' a valid order state
do
Result := Order_states.has (a_status.as_lower)
end
Order_states : ARRAY [STRING]
-- List of valid status states
once
Result := <<
status_unset,
status_submitted,
status_pay, status_payed,
status_cancel, status_canceled,
status_prepare, status_prepared,
status_deliver,
status_completed
>>
Result.compare_objects
end
is_valid_transition (a_order: ORDER; a_new_status: STRING): BOOLEAN
-- Is transition from `a_order.status` to `a_new_status` valid for `a_order`?
local
l_order_status: READABLE_STRING_GENERAL
l_new_status: STRING
do
l_order_status := a_order.status
l_new_status := a_new_status.as_lower
if l_order_status.same_string (l_new_status) then
-- Same status is valid, if it is a valid status
Result := is_valid_status_state (l_new_status)
else
if l_order_status.same_string (status_submitted) then
Result := l_new_status.same_string (status_pay)
or l_new_status.same_string (status_cancel)
elseif l_order_status.same_string (status_pay) then
Result := l_new_status.same_string (status_payed)
elseif l_order_status.same_string (status_cancel) then
Result := l_new_status.same_string (status_canceled)
elseif l_order_status.same_string (status_payed) then
Result := l_new_status.same_string (status_prepared)
elseif l_order_status.same_string (status_prepared) then
Result := l_new_status.same_string (status_deliver)
elseif l_order_status.same_string (status_deliver) then
Result := l_new_status.same_string (status_completed)
end
end
end
is_state_valid_to_update (a_status : STRING) : BOOLEAN
-- Is it possible to update order with status `a_status`?
do
if
a_status.same_string (status_submitted)
or else a_status.same_string (status_pay)
or else a_status.same_string (status_payed)
then
Result := True
end
end
feature -- Constants: order status
status_unset: STRING = "unset"
status_submitted: STRING = "submitted"
status_pay: STRING = "pay"
status_payed: STRING = "payed"
status_cancel: STRING = "cancel"
status_canceled: STRING = "canceled"
status_prepare: STRING = "prepare"
status_prepared: STRING = "prepared"
status_deliver: STRING = "deliver"
status_completed: STRING = "completed"
feature {NONE} -- Access
database: BASIC_DATABASE
feature {NONE} -- Implementation
database_path: PATH
once
create Result.make_from_string ("db")
end
;note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,61 @@
note
description: "Summary description for {SHARED_RESTBUCKS_API}."
date: "$Date$"
revision: "$Revision$"
class
SHARED_RESTBUCKS_API
feature -- Access: bridget to api
has_order (a_id: READABLE_STRING_GENERAL): BOOLEAN
do
Result := api.has_order (a_id)
end
order (a_id: READABLE_STRING_GENERAL): detachable ORDER
do
Result := api.order (a_id)
end
feature -- Element change
submit_order (a_order: ORDER)
-- Submit new order `a_order`.
require
no_id: not a_order.has_id
do
api.submit_order (a_order)
ensure
a_order.has_id
a_order.is_submitted
end
update_order (a_order: ORDER)
-- Update the order to the repository
require
a_order.has_id
do
api.save_order (a_order)
ensure
a_order_with_id: a_order.has_id
end
delete_order (a_order: ORDER)
require
a_order_with_id: a_order.has_id
do
api.delete_order (a_order)
end
feature -- Access
api: RESTBUCKS_API
once
create Result.make
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,135 @@
note
description: "Summary description for {ORDER}."
date: "$Date$"
revision: "$Revision$"
class
ORDER
create
make,
make_empty
feature -- Initialization
make_empty
do
create {ARRAYED_LIST [ORDER_ITEM]} items.make (10)
revision := 0
set_id ("")
status := {RESTBUCKS_API}.status_unset
end
make (a_id: READABLE_STRING_8; a_status: detachable READABLE_STRING_GENERAL)
do
make_empty
set_id (a_id)
if a_status /= Void then
set_status (a_status)
else
check status.same_string_general ({RESTBUCKS_API}.status_unset) end
end
end
feature -- Access
id: IMMUTABLE_STRING_8
location: detachable STRING_32
status: STRING_32
-- Status of the order, see {RESTBUCKS_API}.Order_states
revision: INTEGER
items: LIST [ORDER_ITEM]
feature -- Status report
has_id: BOOLEAN
-- Has valid identifier `id`?
do
Result := not id.is_whitespace and not id.is_case_insensitive_equal ("0")
end
is_submitted: BOOLEAN
do
Result := status.is_case_insensitive_equal_general ({RESTBUCKS_API}.status_submitted)
end
feature -- element change
set_id (a_id: READABLE_STRING_8)
do
if attached {IMMUTABLE_STRING_8} a_id as l_id then
id := l_id
else
create id.make_from_string (a_id)
end
ensure
id_assigned : a_id.same_string (id)
end
set_location (a_location: detachable READABLE_STRING_GENERAL)
do
if a_location = Void then
location := Void
else
create location.make_from_string_general (a_location)
end
ensure
location_assigned: (a_location = Void implies location = Void)
or (a_location /= Void implies attached location as loc and then a_location.same_string (loc))
end
mark_submitted
do
status := {RESTBUCKS_API}.status_submitted
end
set_status (a_status: READABLE_STRING_GENERAL)
do
create status.make_from_string_general (a_status)
ensure
status_assigned : a_status.same_string (status)
end
add_item (a_item: ORDER_ITEM)
require
valid_item: a_item /= Void
do
items.force (a_item)
ensure
has_item : items.has (a_item)
end
add_revision
do
revision := revision + 1
ensure
revision_incremented : old revision + 1 = revision
end
feature -- Report
hash_code: INTEGER_32
-- Hash code value
do
from
items.start
Result := items.item.hash_code
until
items.off
loop
Result:= ((Result \\ 8388593) |<< 8) + items.item.hash_code
items.forth
end
if items.count > 1 then
Result := Result \\ items.count
end
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,93 @@
note
description: "Summary description for {ORDER_ITEM}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER_ITEM
inherit
ORDER_ITEM_VALIDATION
create
make
feature -- Initialization
make (a_name: STRING_32; a_size: STRING_32; a_option: STRING_32; a_quantity: NATURAL_8)
do
set_name (a_name)
set_size (a_size)
set_option (a_option)
set_quantity (a_quantity)
end
feature -- Access
name: READABLE_STRING_32
-- product name type of Coffee(Late, Cappuccino, Expresso)
option: READABLE_STRING_32
-- customization option Milk (skim, semi, whole)
size: READABLE_STRING_32
-- small, mediumm large
quantity: NATURAL_8
feature -- Element Change
set_name (a_name: like name)
require
valid_name: is_valid_coffee_type (a_name)
do
name := a_name
ensure
name_assigned: name.same_string (a_name)
end
set_size (a_size: like size)
require
valid_size: is_valid_size_option (a_size)
do
size := a_size
ensure
size_assigned: size.same_string (a_size)
end
set_option (a_option: like option)
require
valid_option: is_valid_milk_type (a_option)
do
option := a_option
ensure
option_assigned: option.same_string (a_option)
end
set_quantity (a_quantity: NATURAL_8)
do
quantity := a_quantity
ensure
quantity_assigned: quantity = a_quantity
end
feature -- Report
hash_code: INTEGER
-- Hash code value
do
Result := option.hash_code + name.hash_code + size.hash_code + quantity.hash_code
end
invariant
valid_size: is_valid_size_option (size)
valid_coffe: is_valid_coffee_type (name)
valid_customization: is_valid_milk_type (option)
valid_quantity: quantity > 0
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,51 @@
note
description: "Summary description for {ORDER_ITEM_VALIDATION}."
date: "$Date$"
revision: "$Revision$"
class
ORDER_ITEM_VALIDATION
feature -- Access
is_valid_coffee_type (a_type: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_type' a valid coffee type
do
Result := across coffe_types as ic some a_type.is_case_insensitive_equal (ic.item) end
end
Coffe_types: ARRAY [STRING]
-- List of valid Coffee types
once
Result := <<"late", "cappuccino", "expresso">>
end
is_valid_milk_type (a_type: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_type' a valid milk type
do
Result := across milk_types as ic some a_type.is_case_insensitive_equal (ic.item) end
end
Milk_types: ARRAY [STRING]
-- List of valid Milk types
once
Result := <<"skim", "semi", "whole">>
end
is_valid_size_option (a_option: READABLE_STRING_GENERAL): BOOLEAN
-- Is `a_option' a valid size option
do
Result := across size_options as ic some a_option.is_case_insensitive_equal (ic.item) end
end
Size_options: ARRAY [STRING]
-- List of valid Size_options
once
Result := <<"small", "medium", "large">>
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -10,14 +10,10 @@ inherit
WSF_SKELETON_HANDLER
SHARED_DATABASE_API
SHARED_EJSON
SHARED_RESTBUCKS_API
REFACTORING_HELPER
SHARED_ORDER_VALIDATION
WSF_RESOURCE_HANDLER_HELPER
rename
execute_options as helper_execute_options,
@@ -25,9 +21,17 @@ inherit
end
create
make
make_with_router
feature {NONE} -- Initialization
make (a_orderid_path_parameter_name: READABLE_STRING_GENERAL; a_router: WSF_ROUTER)
do
orderid_path_parameter_name := a_orderid_path_parameter_name
make_with_router (a_router)
end
orderid_path_parameter_name: READABLE_STRING_GENERAL
feature -- Execution variables
@@ -155,26 +159,21 @@ feature -- Access
-- If `a_strong' then the strong comparison function must be used.
local
l_id: STRING
l_etag_util: ETAG_UTILS
do
l_id := order_id_from_request (req)
if db_access.orders.has_key (l_id) then
check attached db_access.orders.item (l_id) as l_order then
if l_id /= Void and then has_order (l_id) then
check attached order (l_id) as l_order then
-- postcondition of `has_key'
create l_etag_util
Result := a_etag.same_string (l_etag_util.md5_digest (l_order.out).as_string_32)
Result := a_etag.same_string (order_etag (l_order))
end
end
end
etag (req: WSF_REQUEST): detachable READABLE_STRING_8
-- Optional Etag for `req' in the requested variant
local
l_etag_utils: ETAG_UTILS
do
create l_etag_utils
if attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
Result := l_etag_utils.md5_digest (l_order.out)
Result := order_etag (l_order)
end
end
@@ -232,10 +231,10 @@ feature -- Execution
else
-- the request is of the form /order/{orderid}
l_id := order_id_from_request (req)
if db_access.orders.has_key (l_id) then
if l_id /= Void and then has_order (l_id) then
a_helper.set_resource_exists
if req.is_get_head_request_method then
check attached db_access.orders.item (l_id) as l_order then
check attached order (l_id) as l_order then
-- postcondition `item_if_found' of `has_key'
req.set_execution_variable (Order_execution_variable, l_order)
end
@@ -259,7 +258,7 @@ feature -- GET/HEAD content
do
check attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
-- precondition get_or_head and postcondition order_saved_only_for_get_head of `check_resource_exists' and
if attached {JSON_VALUE} json.value (l_order) as jv then
if attached order_to_json (l_order) as jv then
req.set_execution_variable (Generated_content_execution_variable, jv.representation)
else
req.set_execution_variable (Generated_content_execution_variable, "")
@@ -300,13 +299,13 @@ feature -- DELETE
delete (req: WSF_REQUEST)
-- Delete resource named in `req' or set an error on `req.error_handler'.
local
l_id: STRING
do
l_id := order_id_from_request (req)
if db_access.orders.has_key (l_id) then
if is_valid_to_delete (l_id) then
delete_order (l_id)
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_valid_to_delete (l_order) then
delete_order (l_order)
else
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.method_not_allowed, "DELETE not valid",
"There is conflict while trying to delete the order, the order could not be deleted in the current state")
@@ -352,7 +351,7 @@ feature -- PUT/POST
-- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
save_order (l_order)
submit_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response ("Not a valid order", req, res)
@@ -386,10 +385,10 @@ feature -- PUT/POST
l_id: STRING
do
if attached {READABLE_STRING_8} req.execution_variable (Request_entity_execution_variable) as l_request then
l_order := extract_order_request (l_request)
l_order := order_from_request_input_data (l_request)
if req.is_put_request_method then
l_id := order_id_from_request (req)
if l_order /= Void and then db_access.orders.has_key (l_id) then
if l_order /= Void and then l_id /= Void and then has_order (l_id) then
l_order.set_id (l_id)
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
req.set_execution_variable (Extracted_order_execution_variable, l_order)
@@ -422,153 +421,139 @@ feature -- PUT/POST
feature -- HTTP Methods
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
h: HTTP_HEADER
joc : JSON_ORDER_CONVERTER
etag_utils : ETAG_UTILS
do
create h.make
create joc.make
create etag_utils
json.add_converter(joc)
create h.make
h.put_content_type_application_json
if attached req.request_time as time then
h.add_header ("Date:" +time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
end
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
if attached {JSON_VALUE} json.value (l_order) as jv then
h.put_content_length (jv.representation.count)
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
if attached order_to_json (l_order) as jv then
res.header.put_content_length (jv.representation.count)
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_header_text (h.string)
res.put_string (jv.representation)
end
end
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
h: HTTP_HEADER
l_msg : STRING
l_location : STRING
joc : JSON_ORDER_CONVERTER
do
create h.make
create joc.make
json.add_converter(joc)
h.put_content_type_application_json
if attached {JSON_VALUE} json.value (l_order) as jv then
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
h.put_content_length (l_msg.count)
if attached req.http_host as host then
l_location := "http://" + host + req.request_uri + "/" + l_order.id
h.put_location (l_location)
end
if attached req.request_time as time then
h.put_utc_date (time)
end
res.header.put_content_length (l_msg.count)
res.header.put_location (req.absolute_script_url (req.request_uri + "/" + l_order.id))
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.created)
res.put_header_text (h.string)
res.put_string (l_msg)
end
end
feature {NONE} -- URI helper methods
order_id_from_request (req: WSF_REQUEST): STRING
-- Value of "orderid" template URI variable in `req'
require
req_attached: req /= Void
order_id_from_request (req: WSF_REQUEST): detachable STRING_8
do
if attached {WSF_VALUE} req.path_parameter ("orderid") as l_value then
Result := l_value.as_string.value.as_string_8
else
Result := ""
if attached {WSF_STRING} req.path_parameter (orderid_path_parameter_name) as p_id then
Result := p_id.url_encoded_value -- the ORDER id has to be valid STRING 8 value.
end
end
feature {NONE} -- Implementation Repository Layer
retrieve_order ( id : STRING) : detachable ORDER
-- get the order by id if it exist, in other case, Void
is_valid_to_delete (a_order: ORDER): BOOLEAN
-- Is the order identified by `a_id' in a state whre it can still be deleted?
do
Result := db_access.orders.item (id)
end
save_order (an_order: ORDER)
-- save the order to the repository
local
i : INTEGER
do
from
i := 1
until
not db_access.orders.has_key ((db_access.orders.count + i).out)
loop
i := i + 1
end
an_order.set_id ((db_access.orders.count + i).out)
an_order.set_status ("submitted")
an_order.add_revision
db_access.orders.force (an_order, an_order.id)
end
is_valid_to_delete ( an_id : STRING) : BOOLEAN
-- Is the order identified by `an_id' in a state whre it can still be deleted?
do
if attached retrieve_order (an_id) as l_order then
if order_validation.is_state_valid_to_update (l_order.status) then
if attached order (a_order.id) as l_order then
if api.is_state_valid_to_update (l_order.status) then
Result := True
end
end
end
is_valid_to_update (an_order: ORDER) : BOOLEAN
-- Check if there is a conflict while trying to update the order
is_valid_to_update (a_order: ORDER): BOOLEAN
-- Check if there is a conflict while trying to update the order.
do
if attached retrieve_order (an_order.id) as l_order then
if order_validation.is_state_valid_to_update (l_order.status) and then order_validation.is_valid_status_state (an_order.status) and then
order_validation.is_valid_transition (l_order, an_order.status) then
Result := True
end
end
end
update_order (an_order: ORDER)
-- update the order to the repository
do
an_order.add_revision
db_access.orders.force (an_order, an_order.id)
end
delete_order (an_order: STRING)
-- update the order to the repository
do
db_access.orders.remove (an_order)
end
extract_order_request (l_post : STRING) : detachable ORDER
-- extract an object Order from the request, or Void
-- if the request is invalid
local
parser : JSON_PARSER
joc : JSON_ORDER_CONVERTER
do
create joc.make
json.add_converter(joc)
create parser.make_with_string (l_post)
parser.parse_content
if parser.is_valid and then attached parser.parsed_json_value as jv then
if attached {like extract_order_request} json.object (jv, "ORDER") as res then
Result := res
if attached order (a_order.id) as l_existing_order then
if
api.is_state_valid_to_update (l_existing_order.status) and then
api.is_valid_status_state (a_order.status) and then
api.is_valid_transition (l_existing_order, a_order.status)
then
Result := True
end
end
end
-- update_order (an_order: ORDER)
-- -- update the order to the repository
-- do
-- an_order.add_revision
-- db_access.orders.force (an_order, an_order.id)
-- end
-- delete_order (an_order: STRING)
-- -- update the order to the repository
-- do
-- db_access.orders.remove (an_order)
-- end
feature -- Helpers
order_etag (a_order: ORDER): STRING_8
local
etag_utils: ETAG_UTILS
do
create etag_utils
Result := etag_utils.md5_digest (a_order.hash_code.out + a_order.revision.out)
end
order_from_request (req: WSF_REQUEST): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
l_data: STRING
do
create l_data.make (req.content_length_value.to_integer_32)
req.read_input_data_into (l_data)
Result := order_from_request_input_data (l_data)
end
order_from_request_input_data (a_data: READABLE_STRING_8): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
parser : JSON_PARSER
do
create parser.make_with_string (a_data)
parser.parse_content
if
parser.is_valid and then
attached parser.parsed_json_value as jv
then
Result := order_from_json (jv)
end
end
feature {NONE} -- Conversion
order_to_json (obj: ORDER): JSON_VALUE
do
Result := order_serialization.to_json (obj)
end
order_from_json (jv: JSON_VALUE): detachable ORDER
do
if attached {ORDER} order_serialization.from_json (jv, {ORDER}) as o then
Result := o
end
end
order_serialization: JSON_SERIALIZATION
do
create Result
Result.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
end
note
copyright: "2011-2015, Javier Velilla and others"
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,356 @@
note
description: "{ORDER_HANDLER} handle the resources that we want to expose"
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER_HANDLER
inherit
WSF_URI_TEMPLATE_HANDLER
WSF_RESOURCE_HANDLER_HELPER
redefine
do_get,
do_post,
do_put,
do_delete
end
SHARED_RESTBUCKS_API
REFACTORING_HELPER
WSF_SELF_DOCUMENTED_HANDLER
create
make
feature {NONE} -- Initialization
make (a_orderid_path_parameter_name: READABLE_STRING_GENERAL; a_router: WSF_ROUTER)
do
orderid_path_parameter_name := a_orderid_path_parameter_name
end
orderid_path_parameter_name: READABLE_STRING_GENERAL
feature -- Execute
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute request handler
do
execute_methods (req, res)
end
feature -- Request parameters
order_id_from_request (req: WSF_REQUEST): detachable STRING_8
do
if attached {WSF_STRING} req.path_parameter (orderid_path_parameter_name) as p_id then
Result := p_id.url_encoded_value -- the ORDER id has to be valid STRING 8 value.
end
end
feature -- API DOC
api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
feature -- Documentation
mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION
do
create Result.make (m)
if a_request_methods /= Void then
if a_request_methods.has_method_post then
Result.add_description ("URI:/order METHOD: POST")
elseif
a_request_methods.has_method_get
or a_request_methods.has_method_put
or a_request_methods.has_method_delete
then
Result.add_description ("URI:/order/{orderid} METHOD: GET, PUT, DELETE")
end
end
end
feature -- HTTP Methods
do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
-- <Precursor>
do
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_conditional_get (req, l_order) then
handle_resource_not_modified_response ("The resource" + req.percent_encoded_path_info + " does not change", req, res)
else
compute_response_get (req, res, l_order)
end
else
handle_resource_not_found_response ("The following resource" + req.percent_encoded_path_info + " is not found ", req, res)
end
end
is_conditional_get (req : WSF_REQUEST; l_order : ORDER) : BOOLEAN
-- Check if If-None-Match is present and then if there is a representation that has that etag
-- if the representation hasn't changed, we return TRUE
-- then the response is a 304 with no entity body returned.
local
etag_util : ETAG_UTILS
do
if attached req.meta_string_variable ("HTTP_IF_NONE_MATCH") as if_none_match then
create etag_util
if if_none_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
Result := True
end
end
end
compute_response_get (req: WSF_REQUEST; res: WSF_RESPONSE; l_order: ORDER)
local
l_msg : STRING
do
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
res.header.put_content_length (l_msg.count)
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_string (l_msg)
end
end
do_put (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Updating a resource with PUT
-- A successful PUT request will not create a new resource, instead it will
-- change the state of the resource identified by the current uri.
-- If success we response with 200 and the updated order.
-- 404 if the order is not found
-- 400 in case of a bad request
-- 500 internal server error
-- If the request is a Conditional PUT, and it does not mat we response
-- 415, precondition failed.
do
if attached order_id_from_request (req) as l_id then
if has_order (l_id) then
if attached order_from_request (req) as l_order then
if l_order.has_id and then not l_id.same_string (l_order.id) then
-- If request input data define an order id different from the one in request path!
handle_resource_conflict_response ("ERROR: conflict between the url defining order id [" + l_id + "] and request data of order with id [" + l_order.id + "]!", req, res)
else
l_order.set_id (l_id)
if is_valid_to_update (l_order) then
if is_conditional_put (req, l_order) then
update_order (l_order)
compute_response_put (req, res, l_order)
else
handle_precondition_fail_response ("ERROR", req, res)
end
else
--| TODO: build message explaining the conflict to the client!
--| FIXME: Here we need to define the Allow methods
handle_resource_conflict_response ("ERROR: order could not be updated!", req, res)
end
end
else
handle_bad_request_response ("ERROR: invalid request input data!", req, res)
end
else
handle_bad_request_response ("ERROR: Order [" + l_id.out + "] was not found!", req, res)
end
else
handle_bad_request_response ("ERROR: Missing ORDER id information!", req, res)
end
end
is_conditional_put (req: WSF_REQUEST; a_order: ORDER): BOOLEAN
-- Check if If-Match is present and then if `a_order` is matching that etag,
-- if etag from `a_order` is unchanged, return True.
do
if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then
if attached order (a_order.id) as l_order then
if if_match.same_string (order_etag (l_order)) then
Result := True
end
else
Result := False
end
else
Result := True
end
end
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
do
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.header.add_header ("etag:" + order_etag (l_order))
if attached order_to_json (l_order) as jv then
res.header.put_content_length (jv.representation.count)
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_string (jv.representation)
end
end
do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Here we use DELETE to cancel an order, if that order is in state where
-- it can still be canceled.
-- 200 if is ok
-- 404 Resource not found
-- 405 if consumer and service's view of the resouce state is inconsisent
-- 500 if we have an internal server error
do
if
attached order_id_from_request (req) as l_id and then
attached order (l_id) as l_order
then
if is_valid_to_delete (l_order) then
delete_order (l_order)
compute_response_delete (req, res)
else
--| FIXME: Here we need to define the Allow methods
handle_method_not_allowed_response (req.percent_encoded_path_info + "%N Conflict while trying to delete the order! The order could not be deleted in the current state!", req, res)
end
else
handle_resource_not_found_response ("Order not found", req, res)
end
end
compute_response_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
do
res.header.put_content_type_application_json
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.no_content)
end
do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Here the convention is the following.
-- POST is used for creation and the server determines the URI
-- of the created resource.
-- If the request post is SUCCESS, the server will create the order and will response with
-- HTTP_RESPONSE 201 CREATED, the Location header will contains the newly created order's URI
-- if the request post is not SUCCESS, the server will response with
-- HTTP_RESPONSE 400 BAD REQUEST, the client send a bad request
-- HTTP_RESPONSE 500 INTERNAL_SERVER_ERROR, when the server can deliver the request
do
if attached order_from_request (req) as l_order then
submit_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response ("ERROR: invalid request input data!", req, res)
end
end
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
l_msg : STRING
do
res.header.put_content_type_application_json
if attached order_to_json (l_order) as jv then
l_msg := jv.representation
res.header.put_content_length (l_msg.count)
res.header.put_location (req.absolute_script_url (req.request_uri + "/" + l_order.id))
res.header.put_utc_date (create {DATE_TIME}.make_now_utc)
res.set_status_code ({HTTP_STATUS_CODE}.created)
res.put_string (l_msg)
end
end
feature {NONE} -- URI helper methods
get_order_id_from_path (a_path: READABLE_STRING_32): READABLE_STRING_32
do
Result := a_path.split ('/').at (3)
end
feature {NONE} -- Implementation Repository Layer
order_etag (a_order: ORDER): STRING_8
local
etag_utils: ETAG_UTILS
do
create etag_utils
Result := etag_utils.md5_digest (a_order.hash_code.out + a_order.revision.out)
end
is_valid_to_delete (a_order: ORDER): BOOLEAN
-- Is the order identified by `a_id' in a state whre it can still be deleted?
do
if attached order (a_order.id) as l_order then
if api.is_state_valid_to_update (l_order.status) then
Result := True
end
end
end
is_valid_to_update (a_order: ORDER): BOOLEAN
-- Check if there is a conflict while trying to update the order.
do
if attached order (a_order.id) as l_existing_order then
if
api.is_state_valid_to_update (l_existing_order.status) and then
api.is_valid_status_state (a_order.status) and then
api.is_valid_transition (l_existing_order, a_order.status)
then
Result := True
end
end
end
order_from_request (req: WSF_REQUEST): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
l_data: STRING
do
create l_data.make (req.content_length_value.to_integer_32)
req.read_input_data_into (l_data)
Result := order_from_request_input_data (l_data)
end
order_from_request_input_data (a_data: READABLE_STRING_8): detachable ORDER
-- extract an object Order from the request,
-- or Void if the request is invalid.
local
parser : JSON_PARSER
do
create parser.make_with_string (a_data)
parser.parse_content
if
parser.is_valid and then
attached parser.parsed_json_value as jv
then
Result := order_from_json (jv)
end
end
feature {NONE} -- Conversion
order_to_json (obj: ORDER): JSON_VALUE
do
Result := order_serialization.to_json (obj)
end
order_from_json (jv: JSON_VALUE): detachable ORDER
do
if attached {ORDER} order_serialization.from_json (jv, {ORDER}) as o then
Result := o
end
end
order_serialization: JSON_SERIALIZATION
do
create Result
Result.register (create {ORDER_JSON_SERIALIZATION}, {ORDER})
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -0,0 +1,90 @@
note
description : "REST Buck server"
date : "$Date$"
revision : "$Revision$"
class RESTBUCKS_SERVER_EXECUTION
inherit
WSF_ROUTED_SKELETON_EXECUTION
undefine
requires_proxy
end
WSF_NO_PROXY_POLICY
SHARED_RESTBUCKS_API
create
make
feature {NONE} -- Initialization
setup_router
local
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
do
setup_order_handler (router)
create doc.make_hidden (router)
router.handle ("/api/doc", doc, router.methods_GET)
-- Those 2 following routes are not for the REST api, but mainly to make simpler to test this example.
router.handle ("/", create {WSF_URI_AGENT_HANDLER}.make (agent handle_home), router.methods_GET)
router.handle ("/new_order", create {WSF_URI_AGENT_HANDLER}.make (agent handle_new_order), router.methods_GET)
end
setup_order_handler (a_router: WSF_ROUTER)
local
order_handler: ORDER_HANDLER
do
create order_handler.make ("orderid", a_router)
router.handle ("/order", order_handler, router.methods_POST)
router.handle ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
end
feature -- Handler
handle_home (req: WSF_REQUEST; res: WSF_RESPONSE)
local
l_page: WSF_HTML_PAGE_RESPONSE
l_html: STRING
do
create l_page.make
create l_html.make_empty
l_html.append ("<h1>RESTbucks example (a Create-Read-Update-Delete REST API)</h1>%N")
l_html.append ("<ul>")
l_html.append ("<li>To test this example: create <a href=%"/new_order%">new ORDER entry</a> , and get its json representation right away.</li>")
l_html.append ("<li>See the auto-generated <a href=%"/api/doc%">documentation</a>.</li>")
l_html.append ("</ul>")
l_page.set_body (l_html)
l_page.set_title ("RESTbucks example")
res.send (l_page)
end
handle_new_order (req: WSF_REQUEST; res: WSF_RESPONSE)
local
o: ORDER
redir: WSF_REDIRECTION_RESPONSE
do
create o.make_empty
o.set_location ("TakeAway")
o.add_item (create {ORDER_ITEM}.make ("late", "large", "whole", 1))
o.add_item (create {ORDER_ITEM}.make ("expresso", "small", "skim", 2))
submit_order (o)
create redir.make (req.absolute_script_url ("/order/" + o.id))
redir.set_content ("Order " + o.id + " created.", Void)
res.send (redir)
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="client" uuid="D0059CEB-5F5C-4D21-8C71-842BD0F88468">
<target name="client">
<root class="RESTBUCK_CLIENT" feature="make"/>
<file_rule>
<exclude>/.git$</exclude>
<exclude>/.svn$</exclude>
<exclude>/EIFGENs$</exclude>
</file_rule>
<option warning="true" void_safety="all">
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="http_client" location="..\..\..\library\network\http_client\http_client-safe.ecf" readonly="false"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<cluster name="src" location=".\src\" recursive="true"/>
</target>
</system>

View File

@@ -1,154 +0,0 @@
note
description : "Objects that ..."
author : "$Author$"
date : "$Date$"
revision : "$Revision$"
class
RESTBUCK_CLIENT
create
make
feature {NONE} -- Initialization
make
-- Initialize `Current'.
local
h: DEFAULT_HTTP_CLIENT
sess: HTTP_CLIENT_SESSION
resp : detachable HTTP_CLIENT_RESPONSE
l_location : detachable READABLE_STRING_8
body : STRING
do
create h
sess := h.new_session ("http://127.0.0.1:9090")
-- Uncomment the following 2 lines, if you use fiddler2 web debugging tool
-- sess.set_is_debug (True)
-- sess.set_proxy ("127.0.0.1", 8888)
-- Create Order
print ("%N Create Order %N")
resp := create_order (sess)
-- Read the Order
print ("%N Read Order %N")
l_location := resp.header ("Location")
resp := read_order (sess, l_location)
-- Update the Order
if resp /= Void and then attached resp.body as l_body then
body := l_body.as_string_8
body.replace_substring_all ("takeAway", "in Shop")
print ("%N Update Order %N")
resp := update_order (sess, l_location, body)
end
end
update_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8; a_body : STRING): detachable HTTP_CLIENT_RESPONSE
local
context : HTTP_CLIENT_REQUEST_CONTEXT
do
if attached uri as l_uri then
sess.set_base_url (l_uri)
create context.make
context.headers.put ("application/json", "Content-Type")
Result := sess.put ("", context, a_body )
-- Show headers
across
Result.headers as l_headers
loop
print (l_headers.item.name)
print (":")
print (l_headers.item.value)
io.put_new_line
end
-- Show body
print (Result.body)
io.put_new_line
end
end
read_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
do
if attached uri as l_uri then
sess.set_base_url (l_uri)
Result := sess.get ("", Void)
-- Show headers
across
Result.headers as l_headers
loop
print (l_headers.item.name)
print (":")
print (l_headers.item.value)
io.put_new_line
end
-- Show body
print (Result.body)
io.put_new_line
end
end
create_order (sess: HTTP_CLIENT_SESSION) : HTTP_CLIENT_RESPONSE
local
s: READABLE_STRING_8
j: JSON_PARSER
id: detachable STRING
context : HTTP_CLIENT_REQUEST_CONTEXT
do
s := "[
{
"location":"takeAway",
"items":[
{
"name":"Late",
"option":"skim",
"size":"Small",
"quantity":1
}
]
}
]"
create context.make
context.headers.put ("application/json", "Content-Type")
Result := sess.post ("/order", context, s)
-- Show the Headers
across
Result.headers as l_headers
loop
print (l_headers.item.name)
print (":")
print (l_headers.item.value)
io.put_new_line
end
-- Show the Response body
if attached Result.body as m then
create j.make_with_string (m)
j.parse_content
if j.is_valid and then attached j.parsed_json_object as j_o then
if attached {JSON_STRING} j_o.item ("id") as l_id then
id := l_id.item
end
print (m)
io.put_new_line
end
end
end
feature {NONE} -- Implementation
invariant
-- invariant_clause: True
end

View File

@@ -1,26 +0,0 @@
note
description: "Summary description for {DATABASE_API}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
DATABASE_API
create
make
feature -- Initialization
make
do
create orders.make (10)
end
feature -- Access
orders: HASH_TABLE [ORDER, STRING]
;note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,19 +0,0 @@
note
description: "Summary description for {SHARED_DATABASE_API}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
SHARED_DATABASE_API
feature -- Access
db_access: DATABASE_API
once
create Result.make
end
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,90 +0,0 @@
note
description: "Summary description for {ITEM}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ITEM
inherit
ITEM_CONSTANTS
create
make
feature -- Initialization
make ( a_name : STRING_32 ; a_size:STRING_32; a_option: STRING_32; a_quantity:INTEGER_8)
do
set_name (a_name)
set_size (a_size)
set_option (a_option)
set_quantity (a_quantity)
end
feature -- Access
name : STRING
-- product name type of Coffee(Late, Cappuccino, Expresso)
option : STRING
-- customization option Milk (skim, semi, whole)
size : STRING
-- small, mediumm large
quantity :INTEGER
feature -- Element Change
set_name (a_name: STRING)
require
valid_name: is_valid_coffee_type (a_name)
do
name := a_name
ensure
name_assigned : name.same_string(a_name)
end
set_size (a_size: STRING)
require
valid_size : is_valid_size_option (a_size)
do
size := a_size
ensure
size_assigned : size.same_string(a_size)
end
set_option (an_option: STRING)
require
valid_option : is_valid_milk_type (an_option)
do
option := an_option
ensure
option_assigned : option.same_string (an_option)
end
set_quantity (a_quantity: INTEGER)
require
valid_quantity : a_quantity > 0
do
quantity := a_quantity
ensure
quantity_assigned : quantity = a_quantity
end
feature -- Report
hash_code: INTEGER
--Hash code value
do
Result := option.hash_code + name.hash_code + size.hash_code + quantity.hash_code
end
invariant
valid_size : is_valid_size_option (size)
valid_coffe : is_valid_coffee_type (name)
valid_customization : is_valid_milk_type (option)
valid_quantity : quantity > 0
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,54 +0,0 @@
note
description: "Summary description for {ITEM_CONSTANTS}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ITEM_CONSTANTS
feature -- Access
is_valid_coffee_type (a_type: STRING) : BOOLEAN
--is `a_type' a valid coffee type
do
a_type.to_lower
coffe_types.compare_objects
Result := coffe_types.has (a_type)
end
Coffe_types : ARRAY[STRING]
-- List of valid Coffee types
once
Result := <<"late","cappuccino", "expresso">>
end
is_valid_milk_type (a_type: STRING) : BOOLEAN
--is `a_type' a valid milk type
do
a_type.to_lower
milk_types.compare_objects
Result := milk_types.has (a_type)
end
Milk_types : ARRAY[STRING]
-- List of valid Milk types
once
Result := <<"skim","semi", "whole">>
end
is_valid_size_option (an_option: STRING) : BOOLEAN
--is `an_option' a valid size option
do
an_option.to_lower
size_options.compare_objects
Result := size_options.has (an_option)
end
Size_options : ARRAY[STRING]
-- List of valid Size_options
once
Result := <<"small","mediumn", "large">>
end
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,176 +0,0 @@
note
description: "Summary description for {JSON_ORDER_CONVERTER}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
JSON_ORDER_CONVERTER
inherit
JSON_CONVERTER
create
make
feature -- Initialization
make
do
create object.make ("","","")
end
feature -- Access
object : ORDER
value : detachable JSON_OBJECT
feature -- Conversion
from_json (j: attached like value): detachable like object
-- Convert from JSON value. Returns Void if unable to convert
local
s_id, s_location, s_status: detachable STRING_32
q: INTEGER_8
o: ORDER
i : ITEM
l_array : detachable ARRAYED_LIST [JSON_VALUE]
is_valid_from_json : BOOLEAN
do
is_valid_from_json := True
if attached {STRING_32} json.object (j.item (id_key), Void) as l_id then
s_id := s_id
end
if attached {STRING_32} json.object (j.item (location_key), Void) as l_location then
s_location := l_location
end
if attached {STRING_32} json.object (j.item (status_key), Void) as l_status then
s_status := l_status
end
create o.make ("", s_location, s_status)
if attached {JSON_ARRAY} j.item (items_key) as l_val then
l_array := l_val.array_representation
from
l_array.start
until
l_array.after
loop
if attached {JSON_OBJECT} l_array.item_for_iteration as jv then
if attached {INTEGER_8} json.object (jv.item (quantity_key), Void) as l_integer then
q := l_integer
else
q := 0
end
if
attached {STRING_32} json.object (jv.item (name_key), Void) as s_name and then
attached {STRING_32} json.object (jv.item (size_key), Void) as s_key and then
attached {STRING_32} json.object (jv.item (option_key), Void) as s_option
then
if is_valid_item_customization (s_name, s_key, s_option,q) then
create i.make (s_name, s_key, s_option, q)
o.add_item (i)
else
is_valid_from_json := False
end
else
is_valid_from_json := False
end
end
l_array.forth
end
end
if not is_valid_from_json or o.items.is_empty then
Result := Void
else
Result := o
end
end
to_json (o: like object): like value
-- Convert to JSON value
local
ja : JSON_ARRAY
i : ITEM
jv: JSON_OBJECT
do
create Result.make
-- Result.put (json.value (o.id), id_key)
Result.put (json.value (o.location), location_key)
Result.put (json.value (o.status), status_key)
from
create ja.make_empty
o.items.start
until
o.items.after
loop
i := o.items.item_for_iteration
create jv.make
jv.put (json.value (i.name), name_key)
jv.put (json.value (i.size),size_key)
jv.put (json.value (i.quantity), quantity_key)
jv.put (json.value (i.option), option_key)
ja.add (jv)
o.items.forth
end
Result.put (ja, items_key)
end
feature {NONE} -- Implementation
id_key: JSON_STRING
once
create Result.make_from_string ("id")
end
location_key: JSON_STRING
once
create Result.make_from_string ("location")
end
status_key: JSON_STRING
once
create Result.make_from_string ("status")
end
items_key : JSON_STRING
once
create Result.make_from_string ("items")
end
name_key : JSON_STRING
once
create Result.make_from_string ("name")
end
size_key : JSON_STRING
once
create Result.make_from_string ("size")
end
quantity_key : JSON_STRING
once
create Result.make_from_string ("quantity")
end
option_key : JSON_STRING
once
create Result.make_from_string ("option")
end
feature -- Validation
is_valid_item_customization ( name : STRING_32; size: STRING_32; option : STRING_32; quantity : INTEGER_8 ) : BOOLEAN
local
ic : ITEM_CONSTANTS
do
create ic
Result := ic.is_valid_coffee_type (name) and ic.is_valid_milk_type (option) and ic.is_valid_size_option (size) and quantity > 0
end
note
copyright: "2011-2015, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,114 +0,0 @@
note
description: "Summary description for {ORDER}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER
create
make
feature -- Initialization
make ( an_id : detachable STRING_32; a_location: detachable STRING_32; a_status: detachable STRING_32)
do
create {ARRAYED_LIST [ITEM]} items.make (10)
if an_id /= Void then
set_id (an_id)
else
set_id ("")
end
if a_location /= Void then
set_location (a_location)
else
set_location ("")
end
if a_status /= Void then
set_status (a_status)
else
set_status ("")
end
revision := 0
end
feature -- Access
id : STRING_32
location : STRING_32
items: LIST[ITEM]
status : STRING_32
revision : INTEGER
feature -- element change
set_id (an_id : STRING_32)
do
id := an_id
ensure
id_assigned : id.same_string (an_id)
end
set_location (a_location : STRING_32)
do
location := a_location
ensure
location_assigned : location.same_string (a_location)
end
set_status (a_status : STRING_32)
do
status := a_status
ensure
status_asigned : status.same_string (a_status)
end
add_item (a_item : ITEM)
require
valid_item: a_item /= Void
do
items.force (a_item)
ensure
has_item : items.has (a_item)
end
add_revision
do
revision := revision + 1
ensure
revision_incremented : old revision + 1 = revision
end
feature -- Etag
etag : STRING_32
-- Etag generation for Order objects
do
Result := hash_code.out + revision.out
end
feature -- Output
feature -- Report
hash_code: INTEGER_32
-- Hash code value
do
from
items.start
Result := items.item.hash_code
until
items.off
loop
Result:= ((Result \\ 8388593) |<< 8) + items.item.hash_code
items.forth
end
if items.count > 1 then
Result := Result \\ items.count
end
end
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,56 +0,0 @@
note
description: "Summary description for {ORDER_TRANSITIONS}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
ORDER_VALIDATION
feature -- Access
is_valid_status_state (a_status: STRING) : BOOLEAN
--is `a_status' a valid coffee order state
do
a_status.to_lower
Order_states.compare_objects
Result := Order_states.has (a_status)
end
Order_states : ARRAY[STRING]
-- List of valid status states
once
Result := <<"submitted","pay","payed", "cancel","canceled","prepare","prepared","deliver","completed">>
end
is_valid_transition (order:ORDER a_status : STRING) :BOOLEAN
-- Given the current order state, determine if the transition is valid
do
a_status.to_lower
if order.status.same_string ("submitted") then
Result := a_status.same_string ("pay") or a_status.same_string ("cancel") or order.status.same_string (a_status)
elseif order.status.same_string ("pay") then
Result := a_status.same_string ("payed") or order.status.same_string (a_status)
elseif order.status.same_string ("cancel") then
Result := a_status.same_string ("canceled") or order.status.same_string (a_status)
elseif order.status.same_string ("payed") then
Result := a_status.same_string ("prepared") or order.status.same_string (a_status)
elseif order.status.same_string ("prepared") then
Result := a_status.same_string ("deliver") or order.status.same_string (a_status)
elseif order.status.same_string ("deliver") then
Result := a_status.same_string ("completed") or order.status.same_string (a_status)
end
end
is_state_valid_to_update ( a_status : STRING) : BOOLEAN
-- Given the current state `a_status' of an order, is possible to update the order?
do
if a_status.same_string ("submitted") or else a_status.same_string ("pay") or else a_status.same_string ("payed") then
Result := true
end
end
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,19 +0,0 @@
note
description: "Summary description for {SHARED_ORDER_VALIDATION}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
SHARED_ORDER_VALIDATION
feature
order_validation : ORDER_VALIDATION
once
create Result
end
note
copyright: "2011-2012, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,401 +0,0 @@
note
description: "{ORDER_HANDLER} handle the resources that we want to expose"
author: ""
date: "$Date$"
revision: "$Revision$"
class ORDER_HANDLER
inherit
WSF_URI_TEMPLATE_HANDLER
WSF_RESOURCE_HANDLER_HELPER
redefine
do_get,
do_post,
do_put,
do_delete
end
SHARED_DATABASE_API
SHARED_EJSON
REFACTORING_HELPER
SHARED_ORDER_VALIDATION
WSF_SELF_DOCUMENTED_HANDLER
create
make_with_router
feature {NONE} -- Initialization
make_with_router (a_router: WSF_ROUTER)
-- Initialize `router'.
require
a_router_attached: a_router /= Void
do
router := a_router
ensure
router_aliased: router = a_router
end
feature -- Router
router: WSF_ROUTER
-- Associated router that could be used for advanced strategy
feature -- Execute
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute request handler
do
execute_methods (req, res)
end
feature -- API DOC
api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
feature -- Documentation
mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION
do
create Result.make (m)
if a_request_methods /= Void then
if a_request_methods.has_method_post then
Result.add_description ("URI:/order METHOD: POST")
elseif
a_request_methods.has_method_get
or a_request_methods.has_method_put
or a_request_methods.has_method_delete
then
Result.add_description ("URI:/order/{orderid} METHOD: GET, PUT, DELETE")
end
end
end
feature -- HTTP Methods
do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
-- <Precursor>
local
id: STRING
do
if attached req.path_info as l_path_info then
id := get_order_id_from_path (l_path_info)
if attached retrieve_order (id) as l_order then
if is_conditional_get (req, l_order) then
handle_resource_not_modified_response ("The resource" + l_path_info + "does not change", req, res)
else
compute_response_get (req, res, l_order)
end
else
handle_resource_not_found_response ("The following resource" + l_path_info + " is not found ", req, res)
end
end
end
is_conditional_get (req : WSF_REQUEST; l_order : ORDER) : BOOLEAN
-- Check if If-None-Match is present and then if there is a representation that has that etag
-- if the representation hasn't changed, we return TRUE
-- then the response is a 304 with no entity body returned.
local
etag_util : ETAG_UTILS
do
if attached req.meta_string_variable ("HTTP_IF_NONE_MATCH") as if_none_match then
create etag_util
if if_none_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
Result := True
end
end
end
compute_response_get (req: WSF_REQUEST; res: WSF_RESPONSE; l_order: ORDER)
local
h: HTTP_HEADER
l_msg : STRING
etag_utils : ETAG_UTILS
do
create h.make
create etag_utils
h.put_content_type_application_json
if attached {JSON_VALUE} json.value (l_order) as jv then
l_msg := jv.representation
h.put_content_length (l_msg.count)
if attached req.request_time as time then
h.add_header ("Date:" + time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
end
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_header_text (h.string)
res.put_string (l_msg)
end
end
do_put (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Updating a resource with PUT
-- A successful PUT request will not create a new resource, instead it will
-- change the state of the resource identified by the current uri.
-- If success we response with 200 and the updated order.
-- 404 if the order is not found
-- 400 in case of a bad request
-- 500 internal server error
-- If the request is a Conditional PUT, and it does not mat we response
-- 415, precondition failed.
local
l_put: STRING
l_order : detachable ORDER
id : STRING
do
if attached req.path_info as l_path_info then
id := get_order_id_from_path (l_path_info)
l_put := retrieve_data (req)
l_order := extract_order_request(l_put)
if l_order /= Void and then db_access.orders.has_key (id) then
l_order.set_id (id)
if is_valid_to_update(l_order) then
if is_conditional_put (req, l_order) then
update_order( l_order)
compute_response_put (req, res, l_order)
else
handle_precondition_fail_response ("", req, res)
end
else
--| FIXME: Here we need to define the Allow methods
handle_resource_conflict_response (l_put +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res)
end
else
handle_bad_request_response (l_put +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res)
end
end
end
is_conditional_put (req : WSF_REQUEST; order : ORDER) : BOOLEAN
-- Check if If-Match is present and then if there is a representation that has that etag
-- if the representation hasn't changed, we return TRUE
local
etag_util : ETAG_UTILS
do
if attached retrieve_order (order.id) as l_order then
if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then
create etag_util
if if_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
Result := True
end
else
Result := True
end
end
end
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
h: HTTP_HEADER
joc : JSON_ORDER_CONVERTER
etag_utils : ETAG_UTILS
do
create h.make
create joc.make
create etag_utils
json.add_converter(joc)
create h.make
h.put_content_type_application_json
if attached req.request_time as time then
h.add_header ("Date:" +time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
end
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
if attached {JSON_VALUE} json.value (l_order) as jv then
h.put_content_length (jv.representation.count)
res.set_status_code ({HTTP_STATUS_CODE}.ok)
res.put_header_text (h.string)
res.put_string (jv.representation)
end
end
do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Here we use DELETE to cancel an order, if that order is in state where
-- it can still be canceled.
-- 200 if is ok
-- 404 Resource not found
-- 405 if consumer and service's view of the resouce state is inconsisent
-- 500 if we have an internal server error
local
id: STRING
do
if attached req.path_info as l_path_info then
id := get_order_id_from_path (l_path_info)
if db_access.orders.has_key (id) then
if is_valid_to_delete (id) then
delete_order( id)
compute_response_delete (req, res)
else
--| FIXME: Here we need to define the Allow methods
handle_method_not_allowed_response (l_path_info + "%N There is conflict while trying to delete the order, the order could not be deleted in the current state", req, res)
end
else
handle_resource_not_found_response (l_path_info + " not found in this server", req, res)
end
end
end
compute_response_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
local
h : HTTP_HEADER
do
create h.make
h.put_content_type_application_json
if attached req.request_time as time then
h.put_utc_date (time)
end
res.set_status_code ({HTTP_STATUS_CODE}.no_content)
res.put_header_text (h.string)
end
do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Here the convention is the following.
-- POST is used for creation and the server determines the URI
-- of the created resource.
-- If the request post is SUCCESS, the server will create the order and will response with
-- HTTP_RESPONSE 201 CREATED, the Location header will contains the newly created order's URI
-- if the request post is not SUCCESS, the server will response with
-- HTTP_RESPONSE 400 BAD REQUEST, the client send a bad request
-- HTTP_RESPONSE 500 INTERNAL_SERVER_ERROR, when the server can deliver the request
local
l_post: STRING
do
l_post := retrieve_data (req)
if attached extract_order_request (l_post) as l_order then
save_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response (l_post +"%N is not a valid ORDER", req, res)
end
end
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
local
h: HTTP_HEADER
l_msg : STRING
l_location : STRING
joc : JSON_ORDER_CONVERTER
do
create h.make
create joc.make
json.add_converter(joc)
h.put_content_type_application_json
if attached {JSON_VALUE} json.value (l_order) as jv then
l_msg := jv.representation
h.put_content_length (l_msg.count)
l_location := req.absolute_script_url (req.request_uri + "/" + l_order.id)
h.put_location (l_location)
if attached req.request_time as time then
h.put_utc_date (time)
end
res.set_status_code ({HTTP_STATUS_CODE}.created)
res.put_header_text (h.string)
res.put_string (l_msg)
end
end
feature {NONE} -- URI helper methods
get_order_id_from_path (a_path: READABLE_STRING_32) : STRING
do
Result := a_path.split ('/').at (3)
end
feature {NONE} -- Implementation Repository Layer
retrieve_order ( id : STRING) : detachable ORDER
-- get the order by id if it exist, in other case, Void
do
Result := db_access.orders.item (id)
end
save_order (an_order: ORDER)
-- save the order to the repository
local
i : INTEGER
do
from
i := 1
until
not db_access.orders.has_key ((db_access.orders.count + i).out)
loop
i := i + 1
end
an_order.set_id ((db_access.orders.count + i).out)
an_order.set_status ("submitted")
an_order.add_revision
db_access.orders.force (an_order, an_order.id)
end
is_valid_to_delete ( an_id : STRING) : BOOLEAN
-- Is the order identified by `an_id' in a state whre it can still be deleted?
do
if attached retrieve_order (an_id) as l_order then
if order_validation.is_state_valid_to_update (l_order.status) then
Result := True
end
end
end
is_valid_to_update (an_order: ORDER) : BOOLEAN
-- Check if there is a conflict while trying to update the order
do
if attached retrieve_order (an_order.id) as l_order then
if order_validation.is_state_valid_to_update (l_order.status) and then order_validation.is_valid_status_state (an_order.status) and then
order_validation.is_valid_transition (l_order, an_order.status) then
Result := True
end
end
end
update_order (an_order: ORDER)
-- update the order to the repository
do
an_order.add_revision
db_access.orders.force (an_order, an_order.id)
end
delete_order (an_order: STRING)
-- update the order to the repository
do
db_access.orders.remove (an_order)
end
extract_order_request (l_post : STRING) : detachable ORDER
-- extract an object Order from the request, or Void
-- if the request is invalid
local
parser : JSON_PARSER
joc : JSON_ORDER_CONVERTER
do
create joc.make
json.add_converter(joc)
create parser.make_with_string (l_post)
parser.parse_content
if
parser.is_valid and then
attached parser.parsed_json_value as jv
then
if attached {like extract_order_request} json.object (jv, "ORDER") as res then
Result := res
end
end
end
note
copyright: "2011-2017, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end

View File

@@ -1,57 +0,0 @@
note
description : "REST Buck server"
date : "$Date$"
revision : "$Revision$"
class RESTBUCKS_SERVER_EXECUTION
inherit
WSF_ROUTED_SKELETON_EXECUTION
undefine
requires_proxy
redefine
initialize
end
WSF_ROUTED_URI_TEMPLATE_HELPER
WSF_HANDLER_HELPER
WSF_NO_PROXY_POLICY
create
make
feature {NONE} -- Initialization
initialize
do
Precursor
initialize_router
end
setup_router
local
order_handler: ORDER_HANDLER
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
do
create order_handler.make_with_router (router)
router.handle ("/order", order_handler, router.methods_POST)
router.handle ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
create doc.make_hidden (router)
router.handle ("/api/doc", doc, router.methods_GET)
end
note
copyright: "2011-2015, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end