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

@@ -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

@@ -0,0 +1,559 @@
note
description: "{ORDER_HANDLER} handle the resources that we want to expose"
author: ""
date: "$Date$"
revision: "$Revision$"
class ORDER_HANDLER
inherit
WSF_SKELETON_HANDLER
SHARED_RESTBUCKS_API
REFACTORING_HELPER
WSF_RESOURCE_HANDLER_HELPER
rename
execute_options as helper_execute_options,
handle_internal_server_error as helper_handle_internal_server_error
end
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
make_with_router (a_router)
end
orderid_path_parameter_name: READABLE_STRING_GENERAL
feature -- Execution variables
Order_execution_variable: STRING = "ORDER"
-- Execution variable used by application
Generated_content_execution_variable: STRING = "GENERATED_CONTENT"
-- Execution variable used by application
Extracted_order_execution_variable: STRING = "EXTRACTED_ORDER"
-- Execution variable used by application
feature -- Documentation
description: READABLE_STRING_GENERAL
-- General description for self-generated documentation;
-- The specific URI templates supported will be described automatically
do
Result := "Create, Read, Update or Delete an ORDER."
end
feature -- Access
is_chunking (req: WSF_REQUEST): BOOLEAN
-- Will the response to `req' using chunked transfer encoding?
do
-- No.
end
includes_response_entity (req: WSF_REQUEST): BOOLEAN
-- Does the response to `req' include an entity?
-- Method will be DELETE, POST, PUT or an extension method.
do
Result := False
-- At present, there is no support for this except for DELETE.
end
conneg (req: WSF_REQUEST): SERVER_CONTENT_NEGOTIATION
-- Content negotiatior for all requests
once
create Result.make ({HTTP_MIME_TYPES}.application_json, "en", "UTF-8", "identity")
end
mime_types_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<{HTTP_MIME_TYPES}.application_json>>)
Result.compare_objects
end
languages_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Language header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"en">>)
Result.compare_objects
end
charsets_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Charset header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"UTF-8">>)
Result.compare_objects
end
encodings_supported (req: WSF_REQUEST): LIST [STRING]
-- All values for Accept-Encoding header that `Current' can serve
do
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"identity">>)
Result.compare_objects
end
max_age (req: WSF_REQUEST): NATURAL
-- Maximum age in seconds before response to `req` is considered stale;
-- This is used to generate a Cache-Control: max-age header.
-- Return 0 to indicate already expired.
-- Return Never_expires to indicate never expires.
do
-- All our responses are considered stale.
end
is_freely_cacheable (req: WSF_REQUEST): BOOLEAN
-- Should the response to `req' be freely cachable in shared caches?
-- If `True', then a Cache-Control: public header will be generated.
do
-- definitely not!
end
private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
-- Header names intended for a single user.
-- If non-Void, then a Cache-Control: private header will be generated.
-- Returning an empty list prevents the entire response from being served from a shared cache.
do
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
end
non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
-- Header names that will not be sent from a cache without revalidation;
-- If non-Void, then a Cache-Control: no-cache header will be generated.
-- Returning an empty list prevents the response being served from a cache
-- without revalidation.
do
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
end
is_sensitive (req: WSF_REQUEST): BOOLEAN
-- Is the response to `req' of a sensitive nature?
-- If `True' then a Cache-Control: no-store header will be generated.
do
Result := True
-- since it's commercial data.
end
allowed_cross_origins (req: WSF_REQUEST): detachable STRING
-- Value for Access-Control-Allow-Origin header;
-- If supplied, should be a single URI, or the values "*" or "null".
-- This is currently supported only for GET requests, and POSTs that functions as GET.
do
if req.is_get_head_request_method then
Result := "*"
end
end
matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN
-- Is `a_etag' a match for resource requested in `req'?
-- If `a_strong' then the strong comparison function must be used.
local
l_id: STRING
do
l_id := order_id_from_request (req)
if l_id /= Void and then has_order (l_id) then
check attached order (l_id) as l_order then
-- postcondition of `has_key'
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
do
if attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
Result := order_etag (l_order)
end
end
last_modified (req: WSF_REQUEST): detachable DATE_TIME
-- When representation of resource selected in `req' was last modified;
-- SHOULD be set whenever it can reasonably be determined.
do
end
modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN
-- Has resource requested in `req' been modified since `a_date_time' (UTC)?
do
-- We don't track this information. It is safe to always say yes.
Result := True
end
feature -- Measurement
content_length (req: WSF_REQUEST): NATURAL
-- Length of entity-body of the response to `req'
do
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
-- We only call this for GET/HEAD in this example.
Result := l_response.count.as_natural_32
end
end
allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN
-- The resource named in `req' does not exist, and this is a POST. Do we allow it?
do
-- No.
end
feature -- Status report
finished (req: WSF_REQUEST): BOOLEAN
-- Has the last chunk been generated for `req'?
do
-- precondition is never met
end
feature -- Execution
check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER)
-- Call `a_helper.set_resource_exists' to indicate that `req.path_translated'
-- is the name of an existing resource.
-- We also put the order into `req.execution_variable (Order_execution_variable)' for GET or HEAD responses.
local
l_id: STRING
do
if req.is_post_request_method then
a_helper.set_resource_exists
-- because only /order is defined to this handler for POST
else
-- the request is of the form /order/{orderid}
l_id := order_id_from_request (req)
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 order (l_id) as l_order then
-- postcondition `item_if_found' of `has_key'
req.set_execution_variable (Order_execution_variable, l_order)
end
end
end
end
ensure then
order_saved_only_for_get_head: attached {ORDER} req.execution_variable (Order_execution_variable) implies req.is_get_head_request_method
end
feature -- GET/HEAD content
ensure_content_available (req: WSF_REQUEST)
-- Commence generation of response text (entity-body).
-- If not chunked, then this will create the entire entity-body so as to be available
-- for a subsequent call to `content'.
-- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions
-- are used, then this will also generate the chunk extension for the first chunk.
-- We save the text in `req.execution_variable (Generated_content_execution_variable)'
-- We ignore the results of content negotiation, as there is only one possible combination.
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 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, "")
end
end
ensure then
generated_content_set_for_get_head: req.is_get_head_request_method implies
attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable)
end
content (req: WSF_REQUEST): READABLE_STRING_8
-- Non-chunked entity body in response to `req';
-- We only call this for GET/HEAD in this example.
do
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
Result := l_response
end
end
next_chunk (req: WSF_REQUEST): TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8]
-- Next chunk of entity body in response to `req';
-- The second field of the result is an optional chunk extension.
do
-- precondition `is_chunking' is never met, but we need a dummy `Result'
-- to satisfy the compiler in void-safe mode
Result := ["", Void]
end
generate_next_chunk (req: WSF_REQUEST)
-- Prepare next chunk (including optional chunk extension) of entity body in response to `req'.
-- This is not called for the first chunk.
do
-- precondition `is_chunking' is never met
end
feature -- DELETE
delete (req: WSF_REQUEST)
-- Delete resource named in `req' or set an error on `req.error_handler'.
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)
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")
end
else
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.not_found, "DELETE not valid",
"There is no such order to delete")
end
end
delete_queued (req: WSF_REQUEST): BOOLEAN
-- Has resource named by `req' been queued for deletion?
do
-- No
end
feature -- PUT/POST
is_entity_too_large (req: WSF_REQUEST): BOOLEAN
-- Is the entity stored in `req.execution_variable (Request_entity_execution_variable)' too large for the application?
do
-- No. We don't care for this example.
end
check_content_headers (req: WSF_REQUEST)
-- Check we can support all content headers on request entity.
-- Set `req.execution_variable (Content_check_code_execution_variable)' to {NATURAL} zero if OK, or 415 or 501 if not.
do
-- We don't bother for this example. Note that this is equivalent to setting zero.
end
create_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Create new resource in response to a PUT request when `check_resource_exists' returns `False'.
-- Implementor must set error code of 200 OK or 500 Server Error.
do
-- We don't support creating a new resource with PUT. But this can't happen
-- with our router mappings, so we don't bother to set a 500 response.
end
append_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Create new resource in response to a POST request.
-- 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
submit_order (l_order)
compute_response_post (req, res, l_order)
else
handle_bad_request_response ("Not a valid order", req, res)
end
end
check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Check we can support all content headers on request entity.
-- Set `req.execution_variable (Conflict_check_code_execution_variable)' to {NATURAL} zero if OK, or 409 if not.
-- In the latter case, write the full error response to `res'.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
if not is_valid_to_update (l_order) then
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
handle_resource_conflict_response (l_order.out +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res)
end
else
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
--| This ought to be a 500, as if attached should probably be check attached. But as yet I lack a proof.
handle_resource_conflict_response ("There is conflict while trying to update the order, the order could not be update in the current state", req, res)
end
end
check_request (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Check that the request entity is a valid request.
-- The entity is available as `req.execution_variable (Conflict_check_code_execution_variable)'.
-- Set `req.execution_variable (Request_check_code_execution_variable)' to {NATURAL} zero if OK, or 400 if not.
-- In the latter case, write the full error response to `res'.
local
l_order: detachable ORDER
l_id: STRING
do
if attached {READABLE_STRING_8} req.execution_variable (Request_entity_execution_variable) as l_request then
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 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)
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
handle_bad_request_response (l_request +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res)
end
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
req.set_execution_variable (Extracted_order_execution_variable, l_order)
end
else
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
handle_bad_request_response ("Request is not a valid ORDER", req, res)
end
end
update_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Perform the update requested in `req'.
-- Write a response to `res' with a code of 204 or 500.
do
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
update_order (l_order)
compute_response_put (req, res, l_order)
else
handle_internal_server_error (res)
end
end
feature -- HTTP Methods
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
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
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 {NONE} -- Implementation Repository Layer
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
-- 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-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,33 @@
note
description : "REST Buck server"
date : "$Date$"
revision : "$Revision$"
class RESTBUCKS_SERVER
inherit
WSF_DEFAULT_SERVICE [RESTBUCKS_SERVER_EXECUTION]
create
make
feature {NONE} -- Initialization
make
do
set_service_option ("port", 9090)
import_service_options (create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI}.make_from_file ("server.ini"))
make_and_launch
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

@@ -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

@@ -0,0 +1,24 @@
note
description: "Summary description for {ETAG_UTILS}."
date: "$Date$"
revision: "$Revision$"
class
ETAG_UTILS
feature -- Access
md5_digest (a_string: STRING): STRING
-- Cryptographic hash function that produces a 128-bit (16-byte) hash value, based on `a_string'
local
md5: MD5
do
create md5.make
md5.update_from_string (a_string)
Result := md5.digest_as_string
end
note
copyright: "2011-2014, Javier Velilla and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end