hometoolipsToolips
search
search
Toolips

toolips

  • a manic web-development framework

toolips is a software ecosystem that targets the full scope of web-development; from the most basic of APIs to complex full-stack applications. The goal of this project is to solve the two-language problem as it pertains to web-development — allowing all web-development to take place in one Scientific programming language. The head of this ecosystem is the Toolips web-development framework — a web-development framework designed with extensibility and flexibility in mind.

For example, Toolips offers fullstack callbacks through the ToolipsSession extension, enhanced SVG capabilities through the ToolipsSVG extension, and UDP servers through the ToolipsUDP extension. Julia, unlike JavaScript, does not easily run inside of a web-browser on the client side — so our Julia code is entirely server-side and servers like this are typically associated with back-end projects. Toolips is capable of far more than the average framework with extensions, and this includes full-stack development, so with this particular web-framework we are able to manage the front-end from the back-end. Alongside full-stack web-development, Toolips includes a plethora of alluring features:

  • HTTPS capable Can be deployed with SSL.

  • Extensible server platform.

  • Hyper-Dynamic Multiple-Dispatch Routing — The Toolips router can be completely reworked with extensions to offer completely new and exceedingly versatile functionality.

  • Declarative and composable — files, html, Javascript, and CSS templating syntax provided by ToolipsServables.

  • Modular servers — toolips applications are regular Julia Modules, making them easier to migrate and deploy.

  • Versatilility — toolips can be used for all use-cases, from full-stack web-development to simple endpoints.

  • Parallel Computing — Declarative process management provided by parametric processes.

  • Optionally Asynchronous — the Toolips.start! function provides several different modes to start the server in, including asynchronous, single-threaded, and multi-threaded.

  • Multi-Threaded — Toolips has support for high-level multi-threading through the ParametricProcesses Module

Toolips is able to create ...

  • Endpoints

  • File servers

  • Interactive fullstack web applications (using the ToolipSession extension)

  • Other HTTP or TCP servers (e.g. Proxy server, data-base cursor)

  • UDP servers and web-services services (e.g. Systems servers, DNS servers) (using the ToolipsUDP extension for UDP.)


development environment

To get started with Toolips from Julia, you will need to add the package to an environment with Pkg.

using Pkg

# best available build:
Pkg.add("Toolips")

# latest working:
Pkg.add(name = "Toolips", rev = "stable")

# latest dev:
Pkg.add(name = "Toolips", rev = "Unstable")

server modules

Toolips servers only require a Module to run, not a file-system — we can start a server from any Module with Toolips.start!. start! is provided with a Module (your server), an IP4 (the IP and port to start the server on,) and has the optional key-word arguments threads and router_threads.

module ExampleServer
using Toolips

# 'hello world' in Toolips:
main = route("/") do c::Connection
    
write!(c, "hello world!")
end

#   exporting `main` will load the route. `start!` will allow us to start the server.
export main, start!
end

From here, we simply use the server and then start! it.

using Main.ExampleServer

start!(ExampleServer)

start! comes with a number of arguments to consider including the IP. The inverse to start! is kill!.

start!(MyToolipsApp)
kill!(MyToolipsApp)

An IP4 is constructed in a relatively pragmatic way... We provide a String (the address) and an Integer (the port) to :. This may be provided to start!

ip = "127.0.0.1":8000
start!(ExampleServer, "127.0.0.1":9000)

Though a file-system is not required, it may be easier to start from a base project and utilize a file-system or Pkg environment. For this, we may use Toolips.new_app, which takes a String, our project name. We can also provide a ServerTemplate to both start! and new_app in the first position before the other arguments to create or start a certain type of app. For example, the here is a simple ToolipsUDP server

using Toolips; Toolips.new_app("MyToolipsApp")

This will generate a simple base project, demonstrating the basics of Toolips:

module MyToolipsApp
using Toolips
# using Toolips.Components

# extensions
logger = Toolips.Logger()

main 
= route("/") do c::Toolips.AbstractConnection
    
if ~(:clients in c)
        
push!(c.data, :clients => 0)
    
end
    c[:clients] 
*= 1
    client_number 
= string(c[:clients])
    
log(logger, "served client " * client_number)
    
write!(c, "hello client #" * client_number)
end

# make sure to export!
export main, default_404, logger
end # - module MyToolipsApp <3
  • A dev.jl file is provided alongside these projects to automatically start a development server with a simple include. This server may be started with include("dev.jl"), or from Bash or Command Prompt julia -L dev.jl.

  • Note the export of default_404. default_404 is routed to 404 (try Toolips.default_404.path). to replace this response page simply make a new route with the 404 path and export it. default_404 will automatically be used on servers without a 404 route, so this export is reduntant but in place to demonstrate a 404 page.

routing

Toolips features a dynamic three-stage routing system that can change functionality using parametric polymorphism. Two main functions comprise this routing system, route and route!. route is called to create routes, whereas route! is called by the server and routes the incoming Connection to a route. A standard Route is created using the route function, which will take a Function as the first positional argument and a route as the second.

home = route("/") do c::Connection

end

The provided Function will take one positional argument, an AbstractConnection. A standard Toolips route will hold the Connection type in a type parameter. The call above will return a Route{Connection}. With no annotation, this will automatically use any AbstractConnection. This is primarily used for Toolips multiple-dispatch routing.

the connection

A route will be passed a Connection whenever it is routed with route!. A Connection represents a client's entrance into the Function pipeline — each time a request is made to the server. The Connection stores the server's routes in its Connection.routes field and the server's data in its Connection.data field. The most vital function in association with the Connection is write!, which is used to write data to the incoming Connection as a response. Note that write! is not write, as this is a mutating write! — a write on a response cannot be reverted!

There are also several getter methods associated with the Connection, which may be used to retrieve data(click to reveal docstrings):

Here are some other functions for working with the Connection (click to reveal docstrings):

The standard process for using the Connection involves crafting a response from a Route and then writing it to the Connection with write!.

module LandingPage
using Toolips
using Toolips.Components

home 
= route("/") do c::AbstractConnection
    title_box 
= h2(text = "welcome to my page!")
    top_header 
= div("top", align = "center", children = [title_box])
    
style!(top_header, "padding-top" => 25px, "height" => 25percent, "width" => 100percent, 
    
"background-color" => "#1e1e1e")
    content = div("main", align = "left")
    
style!(content, "padding" => 1percent, "width" => 100percent, 
    
"background-color" => "#1e1e1e")
    mainbod = body(children = [top_header, content])
    
write!(c, mainbod)
end

LOGGER 
= Toolips.Logger()
export LOGGER, home
end

the response

Creating a response is a great place to start with Toolips. To create an app, we build a regular Module with Toolips as a dependency.

module SimpleServer
using Toolips

end

We create a route using the route function, which is provided a path (String) and a page (Function). The target will start at / and from there we may add additional paths.

module SimpleServer
using Toolips

home 
= route("/") do c::AbstractConnection

end
# make sure to export!
export home
end

We can write strings as a response with write!

write!(c)

... and we can also use ToolipsServables Component templating or other packages to produce our response.

main = route("/") do c::Toolips.AbstractConnection
    main_box 
= div("main")
    file_button_style 
= style("div.filebutton""padding" => 5percent, "margin" => 2percent, "color" => "#1e1e1e", "font-weight" => "bold", "font-size" => 15pt, 
    "border-radius" => 5px, "border" => "2px solid #1e1e1e", "cursor" => "pointer", "transition" => 850ms)
    file_button_style:"hover":["transform" => "scale(1.05)"]
    
style!(main_box, "position" => "absolute""overflow-x" => "show""width" => 40percent, "height" => 60percent, "padding" => 5percent, 
    
"top" => 0percent, "left" => 0percent)
    
for md_filename in readdir(DOCUMENTS_URI)
        presentable_name 
= replace(md_filename, ".md" => "")
        comp_name 
= replace(presentable_name, " " => "-")
        
push!(main_box, div(comp_name, text = presentable_name, class = "filebutton"))
    
end
    
write!(c, file_button_style)
    
write!(c, body("mainbody", children = [main_box]))
end

templating

Templating for Toolips is provided by ToolipsServables, aliased as Components in Toolips. ToolipsServables provides an API for templating CSS, HTML, and JavaScript from Julia, allowing us to create some pretty complex websites.

Every AbstractComponent (html element, css class or CSS animation,) will have a name. A Style will take a name style pairs:

my_style = style("div.sampleclass""background-color" => "green""color" => "white""font-weight" => "bold""font-size" => 13pt)

A regular Component will take key-word arguments and style pairs. Special amongst these are :children, :extras, and :text.

  • :text is the text of the element

  • :extras are extra components to be written before the element, as opposed to as a child of the element. This is useful for attaching scripts and other things to components.

  • and :children are the children of that element, which will be written inside of it. We add new children by using push!, or by providing them as an argument when we construct the Component.

A Component can also have its style modified using style!, and setindex!/getindex! work to retrieve any of these properties with a Symbol or String. There is also set_children!.

mycomp = h2(text = "hello!")
mycomp 
= Component{:h2}(text = "hello")
style!(mycomp, "font-size" => 12pt, "font-weight" => "bold""color" => "orange")

mycomp[:text] 
= mycomp[:text] * " world"

new_container 
= div("sample-container", children = [mycomp])
set_children!(new_container, [mycomp])

push!(new_container, h3(text = "welcome to my julia website"))
  • There is a lot more this templating can do, but this overview will not go too into detail. For a more exhaustive overview of the features ToolipsServables offers, you can check out the documentation for the package here

  • Also note the use of measures (e.g pt) from ToolipsServables.

files

A File type is also provided by the Components Module, and like the Component this type may be written directly to the Connection. ToolipsServables also features the interpolate! function for interpolating files of different types. For serving files from routes, Toolips includes the mount function. mount will take a key-value pair, the HTTP target path we want to mount to and the file path to mount. This path can be file or directory; in the case of a directory, this call will return a Vector{Route} and in the case of a file it will return a single Route{Connection}. Both of these can be exported to be loaded into the server.

module QuickFileServer
using Toolips

fs 
= mount("/" => "shared_directory")

export fs
end

It is very straightforward, it might also be worth looking into interpolate! from Components, as well.

multiroute

Multiroute is a Toolips feature that implements multiple dispatch routes for different types of incoming connections. We can create multiple handlers for different cases and the router will handle different incoming connections with the appropriate AbstractConnection. The running example for this is the MobileConnection.

module MobileFriendlyServer
using Toolips
using Toolips.Components

home_n 
= route("/") do c::AbstractConnection
    
write!(c, h1(text = "desktop user!"))
end

home_m 
= route("/") do c::MobileConnection
    
write!(c, h1(text = "mobile user!"))
end

home 
= route(home_n, home_m)

export home
end

But with Toolips extensibility, we could easily implement our own. For example, we could easily implement a POST handler using this multiple dispatch technique. For more information on writing Connection extensions, check out the connection extensions section.

routing extensions

Routing in Julia goes through a multi-step pipeline in which an extensible function, route!, is called three times.

    1. route! is called on each server extension (<: of AbstractExtension) that has a route! binding.

    1. route! is called on the Vector{<:AbstractRoute}, routing to the appropriate route.

    1. Finally, route! is then called directly on a route from the previous binding.

Understanding this routing system allows us to easily change it. In order to change extension behavior each time the server routes, we use the first binding. In order to change the router, we would extend the second option and if we wanted to change what a Route does when routed we use the final option. For example, in order to make a hostname router we would use the second and third:

using Toolips
import Toolips: route!

abstract type AbstractHostRoute <: Toolips.AbstractRoute end

mutable struct HostRoute <: AbstractHostRoute
    path
::String
    link
::String
end

route!(c::AbstractConnection, r::AbstractHostRoute= begin
    
proxy_pass!(c, r.link)
end

route!(c::AbstractConnection, vec::Vector{<:AbstractHostRoute}= begin
    host 
= get_host(c)
    
route!(c, vec[host])
end

main 
= HostRoute("sample.com""127.0.0.1:8000")

export main
end

For more information on the first dispatch of route!, see creating server extensions

connection extensions

Another way to extend Toolips is by adding a Connection extension. This takes shape in a new AbstractConnection, which will need to have a binding to write! (or use the default binding,) and bindings to the Connection getter functions. For example, the IOConnection comes with Toolips and is used to translate client data from the header to the other threads in the ProcessManager, so it contains fields for all of this data:

mutable struct IOConnection <: AbstractIOConnection
    stream
::String
    args
::Dict{Symbol, String}
    ip
::String
    post
::String
    route
::String
    method
::String
    data
::Dict{Symbol, Any}
    routes
::Vector{<:AbstractRoute}
    system
::String
    host
::String

And then a binding for each getter

  • Note that if you replicate this behavior (storing the data as fields of your Connection,) making it a sub-type of AbstractIOConnection will automatically bind those getters.

Likewise, if our implementation is a bit simpler we could just use the http.Stream from the original Connection, as is the case for the MobileConnection.

mutable struct MobileConnection{T} <: AbstractConnection
    stream
::Any
    data
::Dict{Symbol, Any}
    routes
::Vector{AbstractRoute}
    
MobileConnection(stream::Any, data::Dict{Symbol, <:Any}, routes::Vector{<:AbstractRoute}= begin
        
new{typeof(stream)}(stream, data, routes)
    
end
end

From here, if we could either implement this custom Connection into our Function pipeline, or write a new binding for convert and convert!. The MobileConnection does the former:

function convert(c::AbstractConnection, routes::Routes, into::Type{MobileConnection})
    
get_client_system(c)[2]::Bool
end

function convert!(c::AbstractConnection, routes::Routes, into::Type{MobileConnection})
    
MobileConnection(c.stream, c.data, routes)::MobileConnection{typeof(c.stream)}
end

With our convert! functions in place, we can create a multi-route with them for our server:

module MobileFriendlyServer
using Toolips
using Toolips.Components

home_n 
= route("/") do c::AbstractConnection
    
write!(c, h1(text = "desktop user!"))
end

home_m 
= route("/") do c::MobileConnection
    
write!(c, h1(text = "mobile user!"))
end

home 
= route(home_n, home_m)

export home
end

Then we may start the server:

start!(MobileFriendlyServer)

controlling servers

As touched on in our initial overview of start!, start! is the inverse of kill!. A call to start will return a ProcessManager. The ProcessManager is provided by ParametricProcesses and makes it easier for Toolips to distribute incoming tasks onto multiple threads. For more information of the process management system behind Toolips, it might be worthwhile to check out the ParametricProcesses documentation.

multi-threading and processes

The start! function gives us several key-word arguments, async, threads, and router_threads will all change the thread mode of our server. async will determine whether or not the server runs asynchronously on the main thread. When set to false, an active server will take the entire thread. threads will determine the total number of workers in our ProcessManager. Finally, router_threads is used to determine the range of threads that should be used when routing the server. On a server with threads set to 1, router_threads will not be used. When router_threads is used, each number in the range below 2 represents execution on the base thread. To elaborate, in the case of -2:3 we would serve the first four incoming requests using the base thread and then use an additional two threads before repeating.

start!(mod::Module = Main, ip::IP4 = ip4_cli(Main.ARGS);
      threads
::Int64 = 1, router_threads::UnitRange{Int64} = -2:threads, router_type::Type{<:AbstractRoute} = AbstractRoute, 
      async
::Bool = true)

Understanding the base thread and how it impacts performance is important. Evaluating everything on the base thread will always be faster, as we do not need to translate all of the data we are working with over to the other thread, unless that thread is already bogged down with a lot of other actions.

There are some key caveats to multi-threading in Toolips, at least when it comes to doing so using the router. A Stream can only be written to by the Base thread. Toolips provides a solution to this with the IOConnection. This allows for data to be translated back to the base thread and written asynchronously. This means that for multi-threading to work, we will need to annotate our routes with AbstractConnection, or some type of converted Connection type. We also need to use a full environment and project for multi-threading to work, otherwise the module and its environment cannot be translated to the other threads. Ensure the website is a regular Julia package, not an in-REPL Module — otherwise you will get an undefined error when a job is distributed.

If all of that is enough, it is also possible to create and distribute processes manually using the ParametricProcesses API through the Connection or directly. Toolips exports the essentials (click to view documentation, and note the Connection dispatches added by Toolips):

A new_job can be distributed to a ProcessManager or Connection directly using these functions.

using Toolips
              
#  disable router multi-threading
pm = start!(Toolips, threads = 8, router_threads = 1:1)

job 
= new_job() do
    
@info "hello world!"
end

assign!(pm, 2, job)
  • ToolipsSession 0.4.5* also has a multi-threading system for callbacks specifically, and this might be better in some projects.

using server extensions

Server extensions are an integral part of Toolips that allows the server to load new advanced capabilities with a single export. In order to use a server extension, start by importing and constructing it — in this case, we will use the Logger — a ServerExtension that comes with Toolips.

module SampleServer
using Toolips

LOGGER 
= Toolips.Logger()
export LOGGER
end

After constructing, just like the routes we will want to export our new extension. Congratulations, the extension is loaded. Here is an example from the Toolips ecosystem using ToolipsSession:

module SampleServer
using Toolips
using Toolips.Components
using ToolipsSession

SESSION 
= ToolipsSession.Session()
LOGGER 
= Toolips.Logger()

home 
= route("/") do c::AbstractConnection
      
log(c, "hello world!")
      mainbod 
= body("main")
      
# session callback using `ToolipsSession`
      on(c, mainbod, "click") do cm::ComponentModifier
            color_choice 
= randn(1:5)
            selected_color 
= ("blue""white""orange""red""green")[color_choice]
            
style!(cm, mainbod, "background-color" => selected_color)
      
end
      
write!(c, mainbod)
end


export SESSION, LOGGER
export home
end

For quick access to on_start and route! functionality, we can also take advantage of the QuickExtension symbolic type. This allows for new extension types to be implemented without creating a new structure.

module SampleServer
using Toolips
import Toolips: route!, on_start

LOAD 
= Toolips.QuickExtension{:load}()

on_start(ext::QuickExtension{:load}, data::Dict{Symbol, Any}, routes::Vector{<:AbstractRoute}= begin
      
@info "The server just started"
end

route!(c::AbstractConnection, qe::QuickExtension{:load}= begin
      
@info "The server just served a client"
end

home 
= route(c -> write!(c, "hello!"), "/")

export LOAD, home
end

creating server extensions

A ServerExtension able to change the server's functionality in a few key ways:

  • The extension can do something each time the server routes using a route! binding.

  • The extension can do something each time the server starts with an on_start binding.

  • And finally, an extension can hold data for the server within its fields.

Creating your own server extension is straightforward, first we make our ServerExtension type,

module ClientTrackServer
using Toolips

mutable struct ClientTracker <: Toolips.ServerExtension
      clients
::Vector{String}
      
ClientTracker() = new(Vector{String}())
end

home 
= route(Toolips.default_404.page, "/")

TRACKER 
= ClientTracker()

export TRACKER, home
end

Then we bind it to any Toolips functions we want to use (on_start, route!),

module ClientTrackServer
using Toolips
# be sure to import, or you will create `ClientTrackServer.on_start` instead 
#    of a new `Method`.
import Toolips: on_start, route!

mutable struct ClientTracker <: Toolips.ServerExtension
      clients
::Vector{String}
      
ClientTracker() = new(Vector{String}())
end

home 
= route(Toolips.default_404.page, "/")

TRACKER 
= ClientTracker()

# our new bindings:
function on_start(ext::ClientTracker, data::Dict{Symbol, Any}, routes::Vector{<:AbstractRoute})
      
# ensure `clients` is left empty (in case of restart).
      ext.clients = Vector{String}()
end

function route!(c::AbstractConnection, ext::ClientTracker)
      ip 
= get_ip(c)
      
# check if the client is currently counted:
      if ~(ip in ext.clients)
            
@info "registering client: $ip"
            
push!(ext.clients, ip)
      
end
      ip 
= nothing
end


client_count 
= route("/clients") do c::AbstractConnection
      
write!(c, length(TRACKER.clients))
end

export TRACKER, home, client_count
end

and we finish by exporting it in our server Module. Note that these bindings are not necessary but optional dependent on the desired functionality of the ServerExtension.

extended servers

While Toolips primarily targets HTTP-based web-development, the package is not just a web-development framework, but also a server-development framework. For UDP servers, check out the ToolipsUDP extension. Toolips includes a built-in TCP server extension. To start from a TCP server template, use new_app(:TCP, name::String) dispatch of new_app.

using Toolips

Toolips.new_app(:TCP"NewServer")

This will give us a nice minimal demonstration:

module NewServer
using Toolips
using Toolips: get_ip4, handler, read_all

main_handler 
= handler() do c::Toolips.SocketConnection
    query 
= read_all(c)
    
write!(c, "hello client!")
end
# (try with `socket = connect(server IP4); write!(socket, "hello server!"); print(String(readavailable(socket)))`
export main_handler, start!
end

The Connection is then replaced with the SocketConnection, and the Route with the Handler. The Handler is an AbstractHandler, created using the handler function. TCP is a Connection-based protocol, and in most cases we would choose to keep this connection open. There is also get_ip4 for getting the connecting port alongside the IP address. The is_closed and is_connected bindings are useful for this.

main_handler = handler() do c::Toolips.SocketConnection
    input_str 
= ""
    
while is_connected(c)
        input_str 
= input_str * input_str
        
if ~(length(input_str) > 2 && input_str[end - 1:end== "!\n")
            continue
        
end
        result 
= input_str[1:end - 3]
        
@warn result
        
if result == "password"
            break
        
end
        
write!(c, "thanks for sending!")
        input_str 
= ""
    
end
    
# second loop?
    while is_connected(c)
      break
    
end
end

There is also a convenience function provided for easily creating a handler, continue_connection.

chifidocs is currently offline; you are viewing a cached version of the site. Some functionality might be missing. Click this message to try for a live version of chifidocs again.