build status coverage report gem version documentation coverage

ATD

Hello! I assume if you're reading this you really want to know about this really cool, interesting new framework that I made. Well, in that case you've come to the right place. ATD is a small modular framework meant to combine the benefits from rails and sinatra. I originally used sinatra, and ran into issues with scaling, and so hence I did the only logical thing and wrote a new framework. I just found that because of sinatra's "build it from the ground up" philosophy, I couldn't make anything too large without making a mass of spaghetti code. That is why this framework allows you to use rails concepts like MVC and moduarly use different components in development and production. It supports the simple sinatra DSL syntax (for the most part) to make small apps, and allows big controllers and models for separation of concerns in larger apps, which is more of a rails philosophy.

Anyways, I've worked really hard on this, and I would love for you to try it out and maybe even contribute! Feel free to reach out to me at [email protected] with questions or concerns, or if it's something concrete you want me to change, just open an issue.

Installation

Pretty simple, the usual gem install atd if you want to use without bundler, or just add it to your Gemfile with bundler. The recommended setup is to use the master branch of the git repo by adding to your Gemfile:

gem 'atd', git: 'https://gitlab.com/izwick-schachter/atd.git', branch: "master"

Or if you want to live on the edge, where all the latest and greatest features are, change branch to "development". Keep in mind, this may sometimes not work, so use at your own risk. When you do this, it's recommended that you go into the git repo and choose a specific commit and lock that in by setting :ref to the commit hash.

The Basics

ATD is structured around a two basic constructs: Apps and Routes.

Apps

An App is a class which is a functional rack app. It has an instance method call as the rack spec requires, and it contains all the things you need to use ATD. An App is just a class that extends ATD::App. That said, for most usages you will never need to know anything about Apps. Unless you need multiple different Apps in the same file, you don't need to touch it. That is because ATD kindly treats main as an App called DefaultApp. All that means is that anything created in main is added to DefaultApp. So, whenever you do something that is not in an App you have created, you can just know in the back of your head that you are impacting DefaultApp.

Routes

The second basic construct is a Route. These are things which you should understand, because these are how ATD processes most of the things you do. Every route belongs to an App, typically DefaultApp as mentioned above. In it's most simple form, a Route simply says "When /whatever is requested, return this file and/or run this code". The syntax will certainly remind you of sinatra:

request "/some/path", "my_file.html.erb"
# Also aliased to:
req "/some/path", "my_file.html.erb"
# And
r "/some/path", "my_file.html.erb"
# It's easier to type

Why not make the method named for the HTTP verbs? Because we want everything to be customizable. By default, calling request will make the route respond to every HTTP verb. If you want it to only respond to some verbs, you have several choices for how you want to do it:

# If you only wanted to respond to GET, POST, and DELETE:
request "/some/path", "my_file.html.erb", respond_to: [:get, :post, :delete]
# Or this other syntax
get post delete "/some/path", "my_file.html.erb"
# Or a combination of the two
get "/some/path", "my_file.html.erb", respond_to: [:post, :delete]
# Or with dots too
get.post.delete "/some/path", "my_file.html.erb"
# Maybe mixed with request:
request.get.post.delete "/some/path", "my_file.html.erb"
# Or if you wanted to ignore those verbs and only respond to PUT and PATCH
request "/some/path", "my_file.html.erb", ignore: [:get, :post, :delete]

N.B. See precompilers for how the file name you pass is manipulated before it is sent out.

You can also pass a block that will be run whenever the Route is matched:

request "/some/path", "my_file.html.erb" do
  puts "Found me!"
end

and if you want to, you can make the blocks return value be the output of the route by omitting "my_file.html.erb". You can also manipulate the output that you passed (see helpers) by modifying the view[:raw] variable.

Settings

This doesn't exist yet, but is in progress in issue #29.

Advanced Routing

For simple project, the basics of routing will work fine. But if you want to create a full fledged application, then you probably are going to need some of the more advanced features, such as precompilation, compilation, compiler options, and using blocks to manipulate output.

Options Hash

The options hash is the hash provided at the end of the argument list when creating a route. For example, in r "/", "some_file", option_A: "Value", option_B: 35 the options hash will be {option_A: "Value", option_B: 35}. Here is a list of options that ATD looks at internally and their default values (other keys can be passed in the options hash, they will just be passed to compilation methods):

status: 200 # Integer > 99 and < 1000. This will be the status code returned unless it is overridden by
status_code: 200 # Same as status, but overrides. A slightly more verbose syntax.
respond_to: nil # A array of HTTP methods as lowercase symbols which the route should respond to.
ignore: nil # A array of HTTP methods as lowercase symbols which the route should not respond to. This takes highest precedence.
precompile: true # This determines if a route will be precompiled. Unless it is == false it will be precompiled.
compile: true # This determines if a route will be compiled. Unless it is == false it will be compiled.

Serving Files

If you want to serve files, just place them in an assets directory in the app directory and pass the file name with the file extension as the second argument to your routes. In the future you will be able to access this through the App settings.

Helpers

In the block passed to a Route there are helpers available to it. You can add new helper methods by adding them to the ATD::Helpers module. By default there are a few defined, and here they are:

http[:request] # The Rack::Request object
http[:response] # The Rack::Response object
http[:view] # The same thing as view[:raw]
http[:method] # The HTTP verb used to access the route
http[:headers] # The headers which will be sent, by default only "content-type".
http[:status_code] # The status code which the app will respond with, by default 200
params # The params hash we all know and love.
view[:raw] # The precompiled (unless precompile:false) and compiled (unless compile:false) view.

Apps

Whenever you create a route, it is given to an App. This isn't apparent when you create a route in main, but even when you do that the route is added to DefaultApp, as you may remember from the intro. If you are using Apps, then when you create a route in an App, that route belongs to the App. When you start the server, it then creates an instance of the App and starts it because it is a rack app. But that is not what the purpose of Apps are.

The intention of Apps are to allow you to use one App as a template, from which you can create many different Apps. An App is not a rack app all by itself. Every instance of an App Class is a rack app. But the rack app doesn't actually start until start is called. This means that you can create an instance of an App, and then you can modify it before starting it. So for example, you can have an App which is impacted by an instance variable:

class MyApp < ATD::App
  attr_accessor :my_name
  request "/", "Hi! This is my_name's App!" do
    view[:raw] = view[:raw].gsub("my_name", @my_name)
  end
end

Which you can then create an instance of and modify the instance variable:

app = MyApp.new
app.my_name = "Fredrick"
app.start

Then when you try to access the website, it will respond to / with Hi! This is Fredrick's App!.

App Creation

To create an app you can use ATD.new("AppName"). It is important to note that ATD.new is not a constructor, although I will refer to it as one. It simply behaves like one because you can use it to "construct" a new app. The app creation process creates a new class which you can open anywhere in your app file, with the name you pass. The name must respond_to?(:to_sym), and must be a valid class name. You must call the constructor before you begin adding routes to the class, or open the class at all.

You can also use the more intuitive way to create an app, which would be by declaring a class which extends ATD::App, like so:

class MyAppClass < ATD::App
  # All of my routes, settings, etc.
end

Starting the App

There are two basic ways to start the app. You can start it by calling AppName.new.start or more simply AppName.start which will create an instance and call start on it, or you can use the more common syntax. For DefaultApp that would be:

request "/", "Hello World!"
start

And for MyApp that would be:

class MyApp < ATD::App
  request "/", "Hello World!"
  start
end

Controllers

Caution:

This entire section is experimental right now and as partial or no support. Don't trust this section and tread carefully.

Because we understand how important it is to have flexibility in how you work, we provide support of a number of different configurations, and one of the ways we do that is with controllers. A controller is simply a module full of methods which can be referenced from a Route by passing controller_name#action instead of the file name or by putting it in the options hash with to: controller_name#action or to: :action, controller: "controller_name".

If you want to add all the methods from one controller to an App you can do that by calling the controller method in the App with the name of the controller as a parameter.

Here is an example of an App using controllers:

module MyController
  def test_route
    return "You've reached test_route"
  end
end

request "/", "MyController#test_route" #=> "You've reached test_route"

Compilation

N.B. At some point in the indefinite future there might (but probably will) be a tilt integration

The Basics

Because ATD attempts to practice separation of concerns, there is a special module, ATD::Compilation which is responsible for dealing with compilation. In ATD there are two types of compilers: First, there are precompilers, which run during the apps startup process and do things like minify assets and compile things which do not need to by dynamic. Second, there are compilers, which deal with dynamic assets and run whenever a Route is reached. To create them you can use the following syntax (the example is for compiling ERB files):

to_compile "erb" do |file = "", *opts|
  ERB.new(file).result
end

Or if you wanted to define a precompiler to remove all the newlines from a JS file:

to_precompile "js" do |file = "", *opts|
  file.gsub("\n", "")
end

If you notices, the precompilers and compilers take the contents of the file as the first argument (file) and they take the compiler options as the second argument (opts). The options are just whatever was passed with the route, for example in r "/", "my_file.erb", hi: true opts == {hi: true}.

Under the Hood

The compilation works by going through the file extensions from last to first and running the compilations for each extension in that order. For example file.html.erb will first be compiled by the "erb" compiler, then the output of the "erb" compiler will be compiled by the "html" compiler.

Precompilation

Precompilers are not sufficiently advanced to get their own special section yet, but will be soon.

Compilation

Compilers are not sufficiently advanced to get their own special section yet, but will be soon.

Documentation

You can find the YARD docs at http://izwick-schachter.gitlab.io/atd/YARD/.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to the gem page on rubygems.org.

Some notes about Semantic Versioning

Semantic versioning is pretty nice. I like it. But, I really only want to hit a major version when it's production ready, so for now will we will follow semver in that bugfix releases (x.x.*) will be backwards compatible, but minor versions will not be. As soon as this is production ready we will hit 1.0.0 and start using semver.

Contributing

Bug reports and merge requests are welcome on GitLab. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

Contribution Policies

Every contribution should correspond to a relevant issue which you keep up to date with notes on what you are working on. Each issues gets branched off of development and is named issue/#{issue_number}. When you are ready to merge it back in, make sure it passes both rubocop and all the tests. Each issue should get additional tests if necessary.