edgecase
Author: StJohn Piano
Published: 2019-04-14
Datafeed Article 100
This article has been digitally signed by Edgecase Datafeed.
This article has been digitally signed by its author.
1085 words - 506 lines - 13 pages





GOAL



In the previous project A simple API: Implementation, the API accepts a string ("hello") and returns a string ("hello").

However, in practice, we would probably want to only accept and return data as JSON-formatted strings.

Goal: Reimplement the API, processing input and output as JSON.




CONTENTS



- Goal
- Contents
- Project Log






PROJECT LOG




Using JSON-formatted data allows us to think of the input and output as language-agnostic objects, to which properties can be attached.

Example additional properties for the input:
- the identity that made the request
- the time at which the request was received by the server
- a data package (e.g. the data from a POST request)

Example additional properties for the output:
- the validity of the request (perhaps the request was incorrectly formatted)
- an error (with an error code and an error message e.g. "You aren't authorised to view this item")
- a data package (e.g. an reasonably large file)

The functionality from the previous project can be preserved. The input object can still have the original URI (e.g. "/api/v1/hello") as a property. The output can still have the original result as a property (e.g. "world").



API = Application Programming Interface
URI = Uniform Resource Identifier


System details:
- Name: Shovel
- Specifications: HP 6005 Pro SFF. 3 GHz x86_64 processor (AMD II x4 B95 Quad Core), 4 GB RAM, 1 TB hard drive. Running CentOS 7.6.1810 (Core).
- More information: New computer: Shovel
- Installed items: GCC 4.8.5, Make 3.82, Vim 7.4, Python 2.7.5, Python 3.3.2, Gnome 3.28.2, gedit 3.28.1.



I'm working in the Gnome GUI.

I'm using:
- gedit for writing this log and the code
- Python 2.7.5 as the code language


Create a new project directory named "a_simple_api_json_inputoutput". Within it, create a new work directory named "work".

Open a terminal and change directory to the work directory.





Let's design the API implementation. We're planning for multiple versions of the API over time, so the main API processor will only look at the URI and transfer the input to the relevant API version.


API implementation:

- work [directory] [contains project]

-- app.py [file] [main program, creates input and sends it to the API]
-- api.py [file] [processes first section of URI and removes it, routes input to the right API version, waits for output, returns output to app.py]

-- api_v1 [directory] [contains version 1 of the API]
--- api.py [file] [processes URI, routes input to the right function, waits for output, returns output to ../api.py]
--- functions.py [file] [the actual core functionality of this API. in this case, return "world" in response to the URI "hello"]






Let's design some test cases.

Case 1:
- Input: "/api/v1/hello"
- Output: "world"

Case 2:
- Input: "/foo"
- Output: "Error: No resource found at URI {/foo}."

Case 3:
- Input: "/api/v1/hola"
- Output: "Error: No resource found at URI {/api/v1/hola}."

Case 4:
- Input: "/api/v1/foo%20bar"
- Output: "Error: URIs cannot contain the character {%}."

Case 5:
- Input: "bar"
- Output: "Error: URIs must start with {/}."








[development occurs here]




Finished.


[spiano@localhost work]$ python app.py


uri: /api/v1/hello
API input (JSON): "/api/v1/hello"
API output (JSON): {"message": "world", "type": "result"}
output message: world

uri: /foo
API input (JSON): "/foo"
API output (JSON): {"message": "Error: No resource found at URI {/foo}.", "code": 0, "type": "error"}
output message: Error: No resource found at URI {/foo}.

uri: /api/v1/hola
API input (JSON): "/api/v1/hola"
API output (JSON): {"message": "Error: No resource found at URI {/api/v1/hola}.", "code": 0, "type": "error"}
output message: Error: No resource found at URI {/api/v1/hola}.

uri: /api/v1/foo%20bar
API input (JSON): "/api/v1/foo%20bar"
API output (JSON): {"message": "Error: URIs cannot contain the character {%}.", "code": 1, "type": "error"}
output message: Error: URIs cannot contain the character {%}.

uri: bar
API input (JSON): "bar"
API output (JSON): {"message": "Error: URIs must start with {/}. URI: {bar}.", "code": 1, "type": "error"}
output message: Error: URIs must start with {/}. URI: {bar}.





Good.




[spiano@localhost work]$ ls -1

api.py
api.pyc
api_v1
app.py

[spiano@localhost work]$ ls -1 api_v1

api.py
api.pyc
functions.py
functions.pyc
__init__.py
__init__.pyc




The rest of this article contains the code.










In the work directory, we have app.py and api.py.






app.py

python 2.7.5
import json
import api


def main():

	uris = [
		"/api/v1/hello",
		"/foo",
		"/api/v1/hola",
		"/api/v1/foo%20bar",
		"bar",
	]
	
	for uri in uris:
	
		input1 = json.dumps(uri)
		
		output1 = api.get_response(uri)
		
		output_obj = json.loads(output1)
		message = output_obj["message"]
		
		print ""
		print "uri:", uri
		print "API input (JSON):", input1
		print "API output (JSON):", output1
		print "output message:", message
		
	print ""
	

if __name__ == "__main__": main()







api.py

python 2.7.5
import json
import api_v1.api as api_v1


def get_response(uri):

	try:
		output = process_uri(uri)
	except Exception as e:
		output = json.dumps({
			"type": "error",
			"code": 1,
			"message": str(e),
		})
		#import traceback, sys
		#exc_type, exc_value, exc_tb = sys.exc_info()
		#traceback.print_exception(exc_type, exc_value, exc_tb)
		
	return output
	
	
def process_uri(uri):

	# default output
	default_output = json.dumps({
		"type": "error",
		"code": 0,
		"message": "Error: No resource found at URI {%s}." % uri, 
	})

	validate_uri(uri)

	# remove first character {/}
	uri = uri[1:]
	
	sections = uri.split("/")
	
	if sections[0] == "api" and sections[1] == "v1":
		sections2 = sections[2:]
		uri2 = "/" + "/".join(sections2)
		output = api_v1.get_response(uri2)
		output_obj = json.loads(output)
		if output_obj["type"] == "error":
			if output_obj["code"] == 0:
				output = default_output
		return output
		
	return default_output
	

def validate_uri(uri):
	
	# first character must be /
	if uri[0] != "/":
		raise ValueError("Error: URIs must start with {/}. URI: {%s}." % uri)
	
	alphabet_lower = "abcdefghijklmnopqrstuvwxyz"
	alphabet_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	digits = "01234567890"
	symbols = "/.-_?=&#"
	permitted_characters = alphabet_lower + alphabet_upper + digits + symbols
	
	for c in uri: # c = character
		if c not in permitted_characters:
			if not (33 <= ord(c) <= 126): # visible ascii glyphs
				c = "[byte ordinal value %d]" % ord(c)
			raise ValueError("Error: URIs cannot contain the character {%s}." % c)











In the api_v1 directory, we have api.py, functions.py, and __init__.py.






api.py

python 2.7.5
import json
import functions as ff


def get_response(uri):

	try:
		output = process_uri(uri)
	except Exception as e:
		output = json.dumps({
			"type": "error",
			"error_code": 1,
			"error_message": str(e), 
		})
	return output


def process_uri(uri):

	# default output
	default_output = json.dumps({
		"type": "error",
		"code": 0,
		"message": "Error: No resource found at URI {%s}." % uri,
	})

	# first character must be /
	if uri[0] != "/":
		raise ValueError("URIs must start with {/}. URI: {%s}." % uri)
	# remove first character
	uri = uri[1:]

	sections = uri.split("/")
	
	if sections[0] == "hello":

		if len(sections) == 1:
			result = ff.hello()
			output = json.dumps({
				"type": "result",
				"message": result,
			})
			return output
			
	return default_output







functions.py

python 2.7.5
def hello():
	return "world"







__init__.py

python 2.7.5