~ ACE Development ~

A Coronium ACE instance needs an "app" directory to connect to during runtime. This folder lives on the actual Docker Host, not in a Coronium ACE container instance.

The default app name on an initial installation is ace

An "app" directory can contain as many modules as you can fit, which in turn can handle many api calls. You can also run additional ACE instances for separated environments via Docker, or duplicate environments for load balancing.

In most cases one instance of ACE will be sufficient, because of the app/module system, which is limited only by disk capacity of the host.

Apps Directory

Your modules will be placed in the /home/<app_name> directory of the Docker Host. You can SFTP, or SCP to this location to manage files.

Example of Docker host ACE directory structure:

/home
  /app_name
    /module_name
      api.lua
      /tpl
        tpl.html
    /module_name
      api.lua
      ...

Input and Output

In essence, ACE is a standard HTTP server, with a custom Lua based API. With this in mind, it's important to understand how the HTTP protocol works.

Except when facilitated by other services, an HTTP interaction with a client has a request and response phase. Once the response phase has taken place, that's the end of that requests lifecycle. Persistence is emulated through data storage, modules, cookies, and other methods.

A client loses state after the response phase. Each request starts the lifecycle again.

When a client makes a request to one of the method routes in your API, they often will send additional data to be processed. Coronium ACE only supports GET and POST requests.

If the client request is GET, then any query string attached to the endpoint url will be parsed out into a hashed Lua table for use in the ace framework. For example:

http://ace.docker.host:12345/users/login?username=pony&password=boy

When this client hits the ACE server the query string is processed into a table like so:

{
  username = "pony",
  password = "boy"
}

This table is then pushed to the proper method route via the in_data parameter of the method.

-- app/users/api.lua

local api = ace.api()

function api.get.login( in_data )
  local username = in_data.username
  local password = in_data.password

  --== Do some auth or something here

  local msg = ace.sf("Good to see you, %s", username)
  return ace.result( msg )
end

return api

As you can see above, the in_data table will have whatever data was passed in from the client.

The functionality is exactly the same for a client POST request, only the system attempts to convert the "posted" body data instead. Coronium ACE requires all posted data is in a JSON encoded format.

-- app/users/api.lua

local api = ace.api()

function api.post.login( in_data )
  local username = in_data.username
  local password = in_data.password

  --== Do more stuff here.

end

return api

The Response Phase

When it's time to return some data back to the client, the most basic return is just a straight table:

-- app/users/api.lua

local api = ace.api()

function api.get.hello( in_data )
  return { greeting = "Hello " .. in_data.username }
end

return api

Or through the use of ACE wraps:

-- app/users/api.lua

local api = ace.api()

function api.get.hello( in_data )
  local username = in_data.username
  local greeting = ace.sf("Glad to meet you, %s", username)

  -- You can use a "wrap" for quick string output (see API)
  return ace.result( greeting )
end

return api

Request URL:

http://your.docker.host:12345/users/hello?username=Steve

Json Payload: {"result":"Glad to meet you, Steve."}

Any Lua table data will converted to JSON before being sent back down the wire.

Request Headers

Sometimes you may need to inspect the incoming headers for auth, etc. You can access the headers by just including an extra parameter for them. The headers are returned in a hashed Lua table.

local api = ace.api()

function api.get.hello( in_data, in_headers )

  -- Print the headers
  for name, value in pairs( in_headers ) do
    print( name, '=>', value )
  end

  return ace.result("Thanks for the headers!")
return api

You can access the header value directly if you already know what you're looking for:

local api = ace.api()

function api.post.login( in_data, in_headers )
  local auth_header = in_headers['X-Auth-Token']

  if auth_header = "12345" then
    return ace.result("You passed!")
  end

  return ace.error("You lose, no Auth", 101)
end

return api

Making ACE Modules

Creating an ACE api module is very simple. As you read on, you'll see that you have just enough control within the framework, while still keeping it small, simple, and fun to work with.

You can also create a 'starter' module by running:

ace new <app_name> <new_mod_name>

Or create one manually by following along below.

A starter ACE api module directory structure looks like:

module_name
  api.lua
  /tpl
  • Keep the module name simple. No spaces. Use common characters. user or products are good examples.

  • The module and method names are publicly visible as part of the url if viewed in a browser.

  • There must be an api.lua in the folder, and it must be named specifically api.lua.

  • Additionally, create a new folder specifically called tpl for any template needs.

Setting up the api.lua

The ace library is baked into the ACE binary at a global level. No need to require it.

Let's look at a possible tools module:

tools
  api.lua
  /tpl

At bare minimum you'll need the following in your api.lua:

-- app/tools/api.lua

--== Get a new api instance from ACE ==--
local api = ace.api()


return api

And now your ready to start adding your api methods to the module.

A simple echo api method that will trigger on a GET request:

-- app/tools/api.lua

local api = ace.api()

--== An echo api GET function
function api.get.echo( in_data )
  return in_data
end

return api

The API endpoint would end up looking something like:

http://your.docker.host:12345/tools/echo?username=Chris

The in_data is the query string parsed into a Lua table. Here we just send the in_data back to the client, which is perfectly valid. Any outgoing data in a table will be JSON encoded automatically. Any incoming data is converted, and placed in a Lua table.

The result sent is JSON by default and will contain {"username":"Chris"}. JSON tools for working with result data are available for practically every programming language.

A note about the in_data

The in_data table object that gets passed into your methods will contain either the parsed query string as a hash table for GET calls, or the content body keyed in a hash table for POST.

The ACE API will only accept a JSON object body for POST requests.

The api.lua file

As you can see, the api.lua is where we set up our endpoints or "routing" methods.

ACE is based on the HTTP request/response protocol. The api file is the first and last point of contact with the client. All client data must enter and exit through the api.lua file. This is very important to remember.

For example, if you wish to add additional code to a module via require then you will need to make sure to pass your final output data back to the api.lua to return to the client.

Always return something to the client, if not, you risk the possibility of leaving a connection hanging.


External Lua Modules

Let's look at an example of using some external code. In this case we have added a utils.lua file. The module directory now looks like so:

tools
  api.lua
  utils.lua
  /tpl

The utils.lua file

-- app/tools/utils.lua

local m = {}

function m.add( a, b )
  return ( a + b ) --this must be returned to api.lua for output.
end

return m

The api.lua file

-- app/tools/api.lua

local utils = require('tools.utils') --module name needed in path
local api = ace.api()

function api.get.add( in_data )
  --collect the incoming data
  local a = in_data.a
  local b = in_data.b

  --call the add utils method
  local r = utils.add(a,b)

  --return the result to client
  return ace.result( r )

end

return api

The API endpoint would look like:

http://your.docker.host:12345/tools/add?a=21&b=12

The JSON payload would be: {"result":33}

Remember to include the module name in the require statement

local utils = require('tools.utils')

Using Content Flags

Coronium ACE is deceptive in its capabilities. The "Coronium" ideal is to provide the simplest approach, chasing away unneeded complexity for most projects, and then allow for more advanced elements as needed.

Content Types

ACE can output three different types of responses to the client; JSON, HTML, or plain text. This is done through the use of content flags.

The default output is JSON and does not need to be specified for normal operations.

A content flag is added to the outgoing response, after the initial payload.

ace.JSON

This flag will try and convert the outgoing content to valid JSON for client consumption.

return { username = "Charles", likes = "Bikes" } [, ace.JSON ] --JSON is the default content type.

ace.HTML

This flag will output HTML rendered content.

return "<h1>Hey There, can you see me?</h1>", ace.HTML

If using a GET route method, the client will see this as rendered HTML.

ace.TEXT

This flag will output plain text. This has a variety of uses for IoT.

return "Here is some plain text even with <i>tags</i>", ace.TEXT

The output will be plain text, and the <i></i> tags will be visible to the client.


Returning Headers

Taking into consideration the examples above, you can also pass header values back to the client. This can be helpful for a variety of uses, including client authentication.

local extra_headers =
{
  ["X-App-Auth-Id"] = "12345",
}

return { food = "Yum" }, ace.HTML, extra_headers

You must provide the content flag if you want to add headers.


HTTP Status Codes

If you need to pass an HTTP status code, which can be helpful for communicating certain information, you can do so after the headers.

local extra_headers =
{
  ["X-App-Auth-Id"] = "12345",
}
local http_status = 404
return {message="Can't find it."}, ace.JSON, extra_headers, http_status

If you don't need additional headers, but you do need to pass a status, then use an empty table as a placeholder:

local http_status = 200 --all good
return {winner=True}, ace.JSON, {}, http_status

Using Flags in Wraps

All of the extra options shown above can also be used in the ACE Wraps. This can be very useful for error returns.

In ace.result:

local headers =
{
  ["X-App-Auth-Id"] = "12345",
}
local http_status = 200
return ace.result("Hello There", ace.JSON, headers, http_status)

In ace.error:

local headers =
{
  ["X-Is-Denied"] = "true",
}
local http_status = 401
return ace.error("<strong>Access Denied</strong>", -99, ace.HTML, headers, http_status)

The extra options are not usable in the generic error return ace.error(). You must provide an error string and code.