Skip to content

Instantly share code, notes, and snippets.

@sancarn
Last active February 8, 2023 03:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sancarn/51b20963543d8b1b948786b56e6007dc to your computer and use it in GitHub Desktop.
Save sancarn/51b20963543d8b1b948786b56e6007dc to your computer and use it in GitHub Desktop.

InfoNet and InfoWorks REST Servers.md

Motivation for a REST Server

Data accessibility

InfoNet and InfoWorks is renound for innaccessibility of data. Business critical asset data is often captured in models, and then never shared with the outside world. Typically the only way to share data with the outside world is via manual exports to a GIS format, and interpreting the data through other system.

This needs to change and a REST API for InfoNet and InfoWorks is an ideal candidate for connectivity of this data.

Moving to the cloud

Many businesses are moving to Cloud-based services for a multitude of reasons. Some of the main reasons I have seen over the past few years are:

  1. Collaborative working
  2. Connectivity of systems
  3. Work from anywhere without need for installing software
  4. On server processing (potentially faster)
  5. Security (no physical storage = lower probability of data leaks) e.g. lost laptops won't cause data leaks.
  6. Lower costs

Although I generally disagree with cloud computing, I can understand why this looks appealing to business leads. Regardless of my personal opinion, cloud computing is likely the future for business systems, so we should get used to it and prepare accordingly.

OOP REST Implementation

My specific REST Implementation was mainly designed to allow quick injection of IExchange API.

Creating a simple web server

You can make a very simple web server as follows:

require 'webrick'
server = WEBrick::HTTPServer.new :Port => 8000
trap 'INT' do
  server.shutdown
end

server.mount_proc '/' do |request, response|
  response.body = request.path
end

server.start

A few examples of URLs and their responses are given below:

http://localserver:8000/          : /
http://localserver:8000/hello/    : /hello
http://localserver:8000/world/    : /world
http://localserver:8000/a/b/c     : /a/b/c

As you can see we can obtain the whole path supplied to the local server by looking in request.path, for this reason we could envisage a system where we supply our base classes and call from there, e.g.

http://localserver:8000/WSApplication/current_network/row_objects/hw_nodes     #Get nodes
http://localserver:8000/WSApplication/current_network/row_objects/hw_conduit   #Get pipes

Equivalent to:

WSApplication.current_network.row_objects("hw_nodes")
WSApplication.current_network.row_objects("hw_conduit")

Methodology

In order to do this we need to keep a "stack" and pop names off the stack, interprate them and change the base object accordingly:

path = "/WSApplication/current_network/row_objects/hw_nodes"
parts = path.split("/")  #=> ["","WSApplication","current_network","row_objects","hw_nodes"]
parts.shift()            #=> ["WSApplication","current_network","row_objects","hw_nodes"]

obj = nil
roots = {
  "WSApplication" => WSApplication
}

#Loop over all url parts
obj=nil
while part = parts.shift()
  if !obj
    #lookup first element in roots map
    obj = roots[part]
  else
    #Ensure object supports the method given
    if obj.respond_to?(part.to_sym)
      #Understand if this method has parameters (think row_objects() which requires table name)
      paramCount = obj.method(part.to_sym).parameters.length
      if paramCount > 0
        #Ensure params are within parts
        if paramCount >= parts.length
          #Populate arguments array
          args = []
          paramCount.times do
            args.push(parts.shift())
          end
          
          #Call object method with arguments and set return value to obj
          obj = obj.public_send(part.to_sym,*args)
        else
          raise "Not enough parameters passed"
        end
      end
    else
      #Error that the method doesn't exist
      raise "Method doesn't exist"
    end
  end
wend

#At this point `obj` is ready to be returned to the body.

At this point obj is ready to be returned, but how should we return the data? Realistically we'd want row_objects to be returned as geoJSON so we can render it on a GIS control e.g. Leaflet.

We can do this via implementing the to_json method ontop of the JSON library.

response.status = 200
response['Content-Type'] = 'text/json'
response.body = obj.to_json

Extend WSRowObject and WSRowObjectCollection with to_json method to provide serialization capability.

class WSOpenNetwork
  def to_json
    return self.tables.select do |table|
      self.row_object_collection(table.name).length > 0
    end.map do |table|
      next {"table" => table.name, "description" => table.description, "rowCount" => self.row_object_collection(table.name).length}
    end
  end
end

class WSRowObjectCollection
  def to_json
    return self.enum_for(:each).to_json
  end
end

class WSRowObject
  def to_json
    #rowData template
    rowData = {
      "type" => "Feature",
      "properties" => {},
      "geometry": {}
    }
    
    #Populate all fields
    self.table_info.fields.each do |field|
      rowData["properties"][field.description] = self[field.name]
    end
    
    #Determine geometry type from supported fields
    fields = self.table_info.fields.map {|f| f.name}
    if fields.include?("x") && fields.include?("y")
      rowData["geometry"]["type"] = "Point"
      rowData["geometry"]["coordinates"] = [self.x,self.y]
    elsif fields.include?("point_array")
      rowData["geometry"]["type"] = "LineString"
      rowData["geometry"]["coordinates"] = self.point_array
    elsif fields.include?("boundary_array")
      rowData["geometry"]["type"] = "Polygon"
      rowData["geometry"]["coordinates"] = self.boundary_array
    end
    return rowData.to_json
  end
end

And at this point as long as we wrap the above method to use WSApplication and WSDatabase we have a full REST API for InfoNet.

Clean Example of API:

class WSOpenNetwork
  def to_json
    return self.tables.select do |table|
      self.row_object_collection(table.name).length > 0
    end.map do |table|
      next {"table" => table.name, "description" => table.description, "rowCount" => self.row_object_collection(table.name).length}
    end
  end
end

class WSRowObjectCollection
  def to_json
    return self.enum_for(:each).to_json
  end
end

class WSRowObject
  def to_json
    #rowData template
    rowData = {
      "type" => "Feature",
      "properties" => {},
      "geometry": {}
    }
    
    #Populate all fields
    self.table_info.fields.each do |field|
      rowData["properties"][field.description] = self[field.name]
    end
    
    #Determine geometry type from supported fields
    fields = self.table_info.fields.map {|f| f.name}
    if fields.include?("x") && fields.include?("y")
      rowData["geometry"]["type"] = "Point"
      rowData["geometry"]["coordinates"] = [self.x,self.y]
    elsif fields.include?("point_array")
      rowData["geometry"]["type"] = "LineString"
      rowData["geometry"]["coordinates"] = self.point_array
    elsif fields.include?("boundary_array")
      rowData["geometry"]["type"] = "Polygon"
      rowData["geometry"]["coordinates"] = self.boundary_array
    end
    return rowData.to_json
  end
end

require_relative 'RESTServer'
server = RESTServer.new({:Port => 8000})
server.register("app", WSApplication)
server.register("db",  WSDatabase)
server.start

Further work

At this point however, with full IExchange the language becomes very verbose:

http://localhost:8000/app/open/st1w444%3A4000%2FMainDatabase/object_by_type_and_id/Collection%20Network/40/open/hw_nodes
http://localhost:8000/app/open/st1w444%3A4000%2FMainDatabase/object_by_type_and_id/Collection%20Network/40/open/hw_conduit

A better approach might look like this:

http://localhost:8000/api/MainDatabase/networks/40/hw_nodes
http://localhost:8000/api/MainDatabase/networks/40/hw_conduit

Of course this is typically incredibly simple, we simply define the necessary class wrappers and shove them into the api methods.


## Final example
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment