From 7a27a36e70f13163bd4a6d94e72b9b14ff88f32f Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Mon, 17 Mar 2008 01:54:48 -0400 Subject: [PATCH 01/27] Not needed for this branch. --- AUTHORS | 1 - README | 122 ----- Rakefile | 153 ------ bin/halcyon | 205 -------- example/authed/simple.client.rb | 21 - example/authed/simple.rb | 40 -- example/pref_manager/README | 47 -- example/pref_manager/client.rb | 67 --- example/pref_manager/config.yml | 25 - example/pref_manager/server.rb | 104 ----- example/simple.client.rb | 17 - example/simple.rb | 37 -- lib/halcyon.rb | 54 --- lib/halcyon/client.rb | 49 -- lib/halcyon/client/base.rb | 261 ----------- lib/halcyon/client/exceptions.rb | 41 -- lib/halcyon/client/router.rb | 106 ----- lib/halcyon/exceptions.rb | 98 ---- lib/halcyon/server.rb | 62 --- lib/halcyon/server/auth/basic.rb | 107 ----- lib/halcyon/server/base.rb | 777 ------------------------------- lib/halcyon/server/exceptions.rb | 41 -- lib/halcyon/server/router.rb | 103 ---- spec/halcyon/error_spec.rb | 55 --- spec/halcyon/router_spec.rb | 32 -- spec/halcyon/server_spec.rb | 105 ----- spec/spec_helper.rb | 21 - 27 files changed, 2751 deletions(-) delete mode 100644 AUTHORS delete mode 100644 README delete mode 100644 Rakefile delete mode 100755 bin/halcyon delete mode 100644 example/authed/simple.client.rb delete mode 100644 example/authed/simple.rb delete mode 100644 example/pref_manager/README delete mode 100644 example/pref_manager/client.rb delete mode 100644 example/pref_manager/config.yml delete mode 100644 example/pref_manager/server.rb delete mode 100644 example/simple.client.rb delete mode 100644 example/simple.rb delete mode 100644 lib/halcyon.rb delete mode 100644 lib/halcyon/client.rb delete mode 100644 lib/halcyon/client/base.rb delete mode 100644 lib/halcyon/client/exceptions.rb delete mode 100644 lib/halcyon/client/router.rb delete mode 100644 lib/halcyon/exceptions.rb delete mode 100644 lib/halcyon/server.rb delete mode 100644 lib/halcyon/server/auth/basic.rb delete mode 100644 lib/halcyon/server/base.rb delete mode 100644 lib/halcyon/server/exceptions.rb delete mode 100644 lib/halcyon/server/router.rb delete mode 100644 spec/halcyon/error_spec.rb delete mode 100644 spec/halcyon/router_spec.rb delete mode 100644 spec/halcyon/server_spec.rb delete mode 100644 spec/spec_helper.rb diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index ec7e140..0000000 --- a/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -* Matt Todd diff --git a/README b/README deleted file mode 100644 index 790ff86..0000000 --- a/README +++ /dev/null @@ -1,122 +0,0 @@ -= Halcyon JSON Server Framework - -A JSON Web Server Framework designed to provide for simple applications dealing -solely with JSON requests and responses from AJAX client applications or for -lightweight server-side message transport, particularly with authentication and -the like. - -== On Rack - -Halcyon is based off of Rack. Rejoice, Rack is amazing. - -== Quick start - -There's not much to Halcyon. I've put a good deal of time into fleshing out the -RDocs so check out the documentation and the example directory. - -Halcyon is the sister project of Aurora SAS, a simple authentication server to -manage authentication, session management, and user roles and permissions. It -is still very early in development (as Halcyon was a prerequisite) but there -should be some interesting code from that project to let you see just what -Halcyon is capable. Stay tuned! - -== Installing with RubyGems - -A Gem of Halcyon is available. You can install it with: - - $ sudo gem install halcyon - -== Usage - -The +halcyon+ command will assist you for running the server. Just run: - - $ halcyon -d -p 3800 example/simple - -You may need to +cd+ into the project directory, or, alternatively, you can -+cp+ the files out into your +tmp+ folder and work from there. If you'd like -to just +cd+, +gem which halcyon+ will tell you where to find the Gem -directory. - -Once you've gotten the server running, pull open your browser, point it to -http://localhost:3800/ and see what happens. Take a look at the source and try -to access the other routes and see how things work. Notice the response in the -browser. - -Once you've familiarized yourself with that, kill the server (Ctl+C) and start -it again without the debugging switch: -d. (+halcyon -h+ for usage help.) - - $ halcyon -p 3800 example/simple - -Now pull it up again in the browser. You'll notice right away that it blocks -all access from any user agent that doesn't meet its requirements (but debug -mode disabled that feature). - -The good news about that is that it reduces a lot of the garbage signals that -a normal server might have to endure, but since we're working with specialized -applications, it's perfectly reasonable to be very stingy about who we talk to. - -Now, pull up IRB and +require+ RubyGems and Halcyon (as halcon/client). Now -run the following: - - >> require 'example/simple.client' - >> s = Simple.new('http://localhost:3800') - >> s.greet("Matt") - >> s.get("/hello/Matt") - >> s.url_for('greet', :name => 'John') - -And that is some very simple stuff you can do with the Client library. - -The Client library is meant to be used in larger applications where a fraction -of functionality requires smaller and faster updates or quicker responses in a -lightweight protocol, perfect for sending authentication information (over -secure channels, of course) or getting updates on various monitoring sources. - -Read more in the RDocs, there's a lot more there to find out. The best way to -learn, though, is to play, and I like to play, so, go for it. - -== Contact - -Please mail bugs, suggestions and patches to . - -You are also welcome to join the #halcyon channel on irc.freenode.net. - -Our website is up so stop by and check out what's going down. Our address is -http://halcyon.rubyforge.org/. On there you will find information about our -mailing list as well, so do stop by. - -== License and Copyright - -Copyright (C) 2007 Matt Todd . - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -== Thanks To - -* Bill Marquette, typo correction, reviewing examples -* Elliott Cable, missing dependency, Thin testing -* ramstedt, Mongrel on JRuby port numericality issue (#14) - -== Links - -Halcyon:: -Aurora:: - -Rack:: -JSON:: - -Matt Todd:: diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 7f7d133..0000000 --- a/Rakefile +++ /dev/null @@ -1,153 +0,0 @@ -$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib"))) - -%w(rubygems rake rake/clean rake/packagetask rake/gempackagetask rake/rdoctask rake/contrib/rubyforgepublisher fileutils pp).each{|dep|require dep} - -include FileUtils - -require 'lib/halcyon' - -project = { - :name => "halcyon", - :version => Halcyon.version, - :author => "Matt Todd", - :email => "chiology@gmail.com", - :description => "A JSON App Server Framework", - :homepath => 'http://halcyon.rubyforge.org', - :bin_files => %w(halcyon), - :rdoc_files => %w(lib), - :rdoc_opts => %w[ - --all - --quiet - --op rdoc - --line-numbers - --inline-source - --title "Halcyon\ Documentation" - --exclude "^(_darcs|spec|pkg|.svn)/" - ], - :dependencies => { - 'json_pure' => '>=1.1.1', - 'rack' => '>=0.2.0', - 'merb' => '>=0.4.1' - }, - :requirements => 'install the json gem to get faster JSON parsing', - :ruby_version_required => '>=1.8.6' -} - -BASEDIR = File.expand_path(File.dirname(__FILE__)) - -spec = Gem::Specification.new do |s| - s.name = project[:name] - s.rubyforge_project = project[:name] - s.version = project[:version] - s.platform = Gem::Platform::RUBY - s.has_rdoc = true - s.extra_rdoc_files = project[:rdoc_files] - s.rdoc_options += project[:rdoc_opts] - s.summary = project[:description] - s.description = project[:description] - s.author = project[:author] - s.email = project[:email] - s.homepage = project[:homepath] - s.executables = project[:bin_files] - s.bindir = "bin" - s.require_path = "lib" - project[:dependencies].each{|dep| - s.add_dependency(dep[0], dep[1]) - } - s.requirements << project[:requirements] - s.required_ruby_version = project[:ruby_version_required] - s.files = (project[:rdoc_files] + %w[Rakefile] + Dir["{spec,lib}/**/*"]).uniq -end - -Rake::GemPackageTask.new(spec) do |p| - p.need_zip = true - p.need_tar = true -end - -desc "Package and Install halcyon" -task :install do - name = "#{project[:name]}-#{project[:version]}.gem" - sh %{rake package} - sh %{sudo gem install pkg/#{name}} -end - -desc "Uninstall the halcyon gem" -task :uninstall => [:clean] do - sh %{sudo gem uninstall #{project[:name]}} -end - -namespace 'spec' do - desc "generate spec" - task :gen do - sh "bacon -r~/lib/bacon/output -rlib/halcyon -rtest/spec_helper spec/**/* -s > spec/SPEC" - end - - desc "run rspec" - task :run do - sh "bacon -r~/lib/bacon/output -rlib/halcyon -rspec/spec_helper spec/**/* -o CTestUnit" - end - - desc "run rspec verbosely" - task :verb do - sh "bacon -r~/lib/bacon/output -rlib/halcyon -rspec/spec_helper spec/**/* -o CSpecDox" - end -end - -desc "Do predistribution stuff" -task :predist => [:chmod, :changelog, :manifest, :rdoc] - -def manifest - require 'find' - paths = [] - manifest = File.new('MANIFEST', 'w+') - Find.find('.') do |path| - path.gsub!(/\A\.\//, '') - next if path =~ /(\.svn|doc|pkg|^\.|MANIFEST)/ - paths << path - end - paths.sort.each do |path| - manifest.puts path - end - manifest.close -end - -desc "Make binaries executable" -task :chmod do - Dir["bin/*"].each { |binary| File.chmod(0775, binary) } - Dir["test/cgi/test*"].each { |binary| File.chmod(0775, binary) } -end - -desc "Generate a MANIFEST" -task :manifest do - manifest -end - -desc "Generate a CHANGELOG" -task :changelog do - sh "svn log > CHANGELOG" -end - -desc "Generate RDoc documentation" -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.options << '--line-numbers' << '--inline-source' << - '--main' << 'README' << - '--title' << 'Halcyon Documentation' << - '--charset' << 'utf-8' - rdoc.rdoc_dir = "doc" - rdoc.rdoc_files.include 'README' - rdoc.rdoc_files.include('lib/halcyon.rb') - rdoc.rdoc_files.include('lib/halcyon/*.rb') - rdoc.rdoc_files.include('lib/halcyon/*/*.rb') -end - -task :pushsite => [:rdoc] do - sh "rsync -avz doc/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/doc/" - sh "rsync -avz site/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/" -end - -desc "find . -name \"*.rb\" | xargs wc -l | grep total" -task :loc do - sh "find . -name \"*.rb\" | xargs wc -l | grep total" -end - -task :default => Rake::Task['spec:run'] diff --git a/bin/halcyon b/bin/halcyon deleted file mode 100755 index 0130b61..0000000 --- a/bin/halcyon +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env ruby -wKU -#-- -# Created by Matt Todd on 2007-10-25. -# Copyright (c) 2007. All rights reserved. -#++ - -# Blatantly stolen from Chris Neukirchen's rackup utility for running Rack -# apps. (Forgive me, it just made too much sense to use your Rack bootstrap -# code for my Rack bootstrap.) - -#-- -# dependencies -#++ - -%w(rubygems halcyon/server optparse).each{|dep|require dep} - -#-- -# default options -#++ - -$debug = false -$test = false -options = Halcyon::Server::DEFAULT_OPTIONS - -#-- -# parse options -#++ - -opts = OptionParser.new("", 24, ' ') do |opts| - opts.banner << "Halcyon, JSON Server Framework\n" - opts.banner << "http://halcyon.rubyforge.org/\n" - opts.banner << "\n" - opts.banner << "Usage: halcyon [options] appname\n" - opts.banner << "\n" - opts.banner << "Put -c or --config first otherwise it will overwrite higher precedence options." - - opts.separator "" - opts.separator "Options:" - - opts.on("-d", "--debug", "set debugging flag (set $debug to true)") { $debug = true } - opts.on("-D", "--Debug", "enable verbose debugging (set $debug and $DEBUG to true)") { $debug = true; $DEBUG = true } - opts.on("-w", "--warn", "turn warnings on for your script") { $-w = true } - - opts.on("-I", "--include PATH", "specify $LOAD_PATH (multiples OK)") do |path| - $:.unshift(*path.split(":")) - end - - opts.on("-r", "--require LIBRARY", "require the library, before executing your script") do |library| - require library - end - - opts.on("-c", "--config PATH", "load configuration (YAML) from PATH") do |conf_file| - if File.exist?(conf_file) - require 'yaml' - - # load the config file - begin - conf = YAML.load_file(conf_file) - rescue Errno::EACCES - abort("Can't access #{conf_file}, try 'sudo #{$0}'") - end - - # store config file path so SIGHUP and SIGUSR2 will reload the config in case it changes - options[:config_file] = conf_file - - # parse config - case conf - when String - # config file given was just the commandline options - ARGV.replace(conf.split) - opts.parse! ARGV - when Hash - conf.symbolize_keys! - options = options.merge(conf) - when Array - # TODO (MT) support multiple servers (or at least specifying which - # server's configuration to load) - warn "Your configuration file is setup for multiple servers. This is not a supported feature yet." - warn "However, we've pulled the first server entry as this server's configuration." - # an array of server configurations - # default to the first entry since multiple server configurations isn't - # precisely worked out yet. - options = options.merge(conf[0]) - else - abort "Config file in an unsupported format. Config files must be YAML or the commandline flags" - end - else - abort "Config file failed to load. #{conf_file} was not found. Correct the path and try again." - end - end - - opts.on("-s", "--server SERVER", "serve using SERVER (default: #{options[:server]})") do |serv| - options[:server] = serv - end - - opts.on("-o", "--host HOST", "listen on HOST (default: #{options[:host]})") do |host| - options[:host] = host - end - - opts.on("-p", "--port PORT", "use PORT (default: #{options[:port]})") do |port| - options[:port] = port - end - - opts.on("-l", "--logfile PATH", "log access to PATH (default: #{options[:log_file]})") do |log_file| - options[:log_file] = log_file - end - - opts.on("-L", "--loglevel LEVEL", "log level (default: #{options[:log_level]})") do |log_file| - options[:log_level] = log_file - end - - opts.on("-P", "--pidfile PATH", "save PID to PATH (default: #{options[:pid_file]})") do |log_file| - options[:pid_file] = log_file - end - - opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{options[:environment]})") do |env| - options[:environment] = env - end - - opts.on_tail("-h", "--help", "Show this message") do - puts opts - exit - end - - opts.on_tail("-v", "--version", "Show version") do - # require 'halcyon' - puts "Halcyon #{Halcyon::Server.version}" - exit - end - - begin - opts.parse! ARGV - rescue OptionParser::InvalidOption => e - abort "You used an unsupported option. Try: halcyon -h" - end -end - -abort "Halcyon needs an app to run. Try: halcyon -h" if ARGV.empty? -options[:app] = ARGV.shift - -#-- -# load app -#++ - -if !File.exists?("#{options[:app]}.rb") - abort "Halcyon did not find the app #{options[:app]}. Check your path and try again." -end - -require options[:app] -appname = File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize} -begin - app = Object.const_get(appname) -rescue NameError => e - abort "Unable to load #{appname}. Please ensure your server is so named." -end - -#-- -# prepare server -#++ -begin - # attempt to require the server - begin; require options[:server].capitalize; rescue LoadError; end - - # get the appropriate Rack Handler - server = Rack::Handler.const_get(options[:server].capitalize) -rescue NameError - servers = { - 'cgi' => 'CGI', - 'fastcgi' => 'FastCGI', - 'lsws' => 'LSWS', - 'mongrel' => 'Mongrel', - 'webrick' => 'WEBrick', - 'thin' => 'Thin' - } - abort "Unsupported server (missing Rack Handler). Did you mean to specify #{options[:server]}?" unless servers.key? options[:server] - server = Rack::Handler.const_get(servers[options[:server]]) -end - -#-- -# prepare app environment -#++ - -case options[:environment] -when "development" - app = Rack::Builder.new { - use Rack::CommonLogger, STDERR unless server.name =~ /CGI/ - use Rack::ShowExceptions - use Rack::Reloader - use Rack::Lint - run app.new(options) - }.to_app -when "deployment" - app = Rack::Builder.new { - use Rack::CommonLogger, STDERR unless server.name =~ /CGI/ - run app.new(options) - }.to_app -else - app = app.new(options) -end - -#-- -# start server -#++ - -server.run app, :Port => Integer(options[:port]) diff --git a/example/authed/simple.client.rb b/example/authed/simple.client.rb deleted file mode 100644 index 99a1add..0000000 --- a/example/authed/simple.client.rb +++ /dev/null @@ -1,21 +0,0 @@ -%w(rubygems halcyon/client).each{|dep|require dep} -class Simple < Halcyon::Client::Base - route do |r| - r.match('/user/show/:id').to(:module => 'user', :action => 'show') - r.match('/show/:id').to(:action => 'show') - r.match('/hello/:name').to(:action => 'greet') - r.match('/wink').to(:action => 'wink') - r.match('/').to(:action => 'index') - {:action => 'what_are_you_looking_for?'} - end - def headers(req) - req['Authorization'] ||= 'Basic cnVwZXJ0OnNlY3JldA==' - req - end - def greet(name) - get("/hello/#{name}")[:body] - end - def hi(name) - url_for('greet', :name => name) - end -end diff --git a/example/authed/simple.rb b/example/authed/simple.rb deleted file mode 100644 index 2dc218a..0000000 --- a/example/authed/simple.rb +++ /dev/null @@ -1,40 +0,0 @@ -%w(rubygems halcyon/server).each{|dep|require dep} -class Simple < Halcyon::Server::Auth::Basic - basic_auth :only => [:greet] do |username, password| - [username, password] == ['rupert', 'secret'] - end - route do |r| - r.match('/user/show/:id').to(:module => 'user', :action => 'show') - r.match('/show/:id').to(:action => 'show') - r.match('/hello/:name').to(:action => 'greet') - r.match('/wink').to(:action => 'wink') - r.match('/').to(:action => 'index') - {:action => 'what_are_you_looking_for?'} - end - - def greet - ok("Hello #{params[:name]}!") - end - def wink - ok("I'm winking at you right now.") - end - def index - {:status => 200, :body => 'Wish you were cooler.'} - end - - user do - def show - {:status => 200, :body => "You request: #{params[:id]}"} - end - end - - def show - {:status => 200, :body => "This method does not conflict with the show method in the user module."} - end - - # custom 404 error handler - def what_are_you_looking_for? - raise Exceptions::NotFound.new(404, 'Not Found; You did not find what you were expecting because it is not here. What are you looking for?') - end -end -Rack::Handler::Mongrel.run Simple.new, :Port => 3801 if __FILE__ == $0 diff --git a/example/pref_manager/README b/example/pref_manager/README deleted file mode 100644 index b983dfa..0000000 --- a/example/pref_manager/README +++ /dev/null @@ -1,47 +0,0 @@ -= Preference Manager - -== Introduction - -This is a oversimplified, quickly (and poorly) designed preference manager. - -Really it just shows you have to get started passing valuable data back and -forth between the client and the server. - -== Apologies - -I'm certainly sorry that it isn't top tier application design, you'll forgive -me I hope for not spending a great deal of time on this example. I do promise -to address it in the future and make it sharp, but for now my apologies will -have to suffice. - -== TODO - -* Clean up the logic and model so that it makes more sense and has more real- - world sanity. -* Make the user feel like they're talking to an ActiveRecord object, for - instance, allowing them to perform the various CRUD functionality. - -== Author - -Matt Todd (chiology@gmail.com) - -== License and Copyright - -Copyright (C) 2007 Matt Todd . - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/example/pref_manager/client.rb b/example/pref_manager/client.rb deleted file mode 100644 index 86e0a2a..0000000 --- a/example/pref_manager/client.rb +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env ruby -wKU - -%w(rubygems halcyon/client).each{|dep|require dep} - -$port = 4447 if $0 == __FILE__ - -class PrefManager < Halcyon::Client::Base - def find(user) - get("/u/#{user}/prefs")[:body] - end - def create(user, pref, value) - put("/u/#{user}/p/#{pref}", {:value => value})[:body] - end - def read(user, pref) - get("/u/#{user}/p/#{pref}")[:body] - end - def update(user, pref, value) - post("/u/#{user}/p/#{pref}", {:value => value})[:body] - end - def delete(user, pref) - delete("/u/#{user}/p/#{pref}")[:body] - end -end - -class Pref - def initialize(user, pref) - @@manager ||= PrefManager.new("http://localhost:#{$port}") - @user = user - @pref = pref - @value = @@manager.read(@user, @pref)[:value] - end - def self.find(user, pref) - self.new(user, pref) - end - def set(value) - @value = value - end - def get - @value - end - def method_missing(name, *params) - case name.to_s - when "#{@pref}" - @value - when "#{@pref}=" - @value = params[0] - else - super - end - end - def save - @@manager.update(@user, @pref, @value) - end - def destroy - @@manager.delete(@user, @pref) - end -end - -if $0 == __FILE__ - users = ['mtodd','chris2','aurora','kate','jpatterson'] - delivery_types = [:digest,:full,:none] - users.each do |user| - pref = Pref.find(user,'email') - pref.set delivery_types[rand(delivery_types.length)] - pref.save - end -end diff --git a/example/pref_manager/config.yml b/example/pref_manager/config.yml deleted file mode 100644 index 4542ffb..0000000 --- a/example/pref_manager/config.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Base config (standard options) -host: localhost -port: 4447 -server: mongrel -pid_file: /tmp/pref_manager.{port}.pid -log_file: /tmp/pref_manager.log -log_level: warn -environment: none - -# App specific configurations -# can be anything in any structure, but good practice would probably stick -# any app-specific configuration options into a sub-hash... -manager: - db: /tmp/prefs.db.yml - some_key: some value - other_key: other value - num: 12 - ary: - - 1 - - 2 - - yes - - hooray? - hsh: - ooh: baby -# this would be accessible as @config[:manager][:some_key], etc diff --git a/example/pref_manager/server.rb b/example/pref_manager/server.rb deleted file mode 100644 index 72e5447..0000000 --- a/example/pref_manager/server.rb +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env ruby -wKU - -%w(rubygems halcyon/server yaml/store).each{|dep|require dep} - -# Handles the persistence of preferences. -class Pref - - attr_accessor :prefs - - # Sets up the database - def self.load_db(db) - @db = YAML::Store.new(db) - end - - # Retrieves the database - def self.get_db - @db - end - - # Connects to the datastore and loads up the preferences - def initialize(user) - @user = user - @@prefs ||= self.class.get_db - @@prefs.transaction(true) do - @prefs = @@prefs.fetch(@user, {}) - end - end - - # Convenience method to say "user.name" - def name - @user - end - - # Saves the current prefs into the datastore - def save - @@prefs.transaction do - @@prefs[@user] = @prefs - end - end - - # Accesses current preference value - def [] key - @prefs[key] - end - - # Sets current preference value - def []= key, value - @prefs[key] = value - end - -end - -# The Halcyon server, exposing the +/u/:user/p/:pref+ address as a HUB for -# various user's preference actions, essentially the CRUD functionality. -class Server < Halcyon::Server::Base - route do |r| - r.match('/u/:user/p/:pref').to(:action => 'pref') - r.match('/u/:user/prefs').to(:action => 'prefs') - end - - def startup - # app setup - Pref.load_db(@config[:manager][:db]) - end - - # Retreives all of the preferences for a given user - def prefs - ok Pref.new(params[:user]).prefs - end - - # Handles preference CRUD - def pref - # get data - user = Pref.new(params[:user]) - pref = params[:pref] - value = user[pref] - - # dispatch - case method - when :get - @logger.debug "read #{pref} for #{user.name}" - ok :user => user.name, :pref => pref, :value => value - when :post - @logger.debug "update #{pref} for #{user.name}" - value = user[pref] = @req.POST['value'] - user.save - ok :user => user.name, :pref => pref, :value => value - when :put - @logger.debug "create #{pref} for #{user.name}" - value = user[pref] = @req.POST['value'] - user.save - ok :user => user.name, :pref => pref, :value => value - when :delete - @logger.debug "delete #{pref} for #{user.name}" - value = user[pref] = nil - user.save - ok :user => user.name, :pref => pref, :value => value - else - @logger.debug "Weird request made with an unknown request method: #{method}" - raise Exceptions.lookup(406) # Not Acceptable - end - end - -end diff --git a/example/simple.client.rb b/example/simple.client.rb deleted file mode 100644 index 8c5e837..0000000 --- a/example/simple.client.rb +++ /dev/null @@ -1,17 +0,0 @@ -%w(rubygems halcyon/client halcyon/client/base).each{|dep|require dep} -class Simple < Halcyon::Client::Base - route do |r| - r.match('/user/show/:id').to(:module => 'user', :action => 'show') - r.match('/show/:id').to(:action => 'show') - r.match('/hello/:name').to(:action => 'greet') - r.match('/wink').to(:action => 'wink') - r.match('/').to(:action => 'index') - {:action => 'what_are_you_looking_for?'} - end - def greet(name) - get("/hello/#{name}")[:body] - end - def hi(name) - url_for('greet', :name => name) - end -end diff --git a/example/simple.rb b/example/simple.rb deleted file mode 100644 index 7bca0b0..0000000 --- a/example/simple.rb +++ /dev/null @@ -1,37 +0,0 @@ -%w(rubygems halcyon/server).each{|dep|require dep} -class Simple < Halcyon::Server::Base - route do |r| - r.match('/user/show/:id').to(:module => 'user', :action => 'show') - r.match('/show/:id').to(:action => 'show') - r.match('/hello/:name').to(:action => 'greet') - r.match('/wink').to(:action => 'wink') - r.match('/').to(:action => 'index') - {:action => 'what_are_you_looking_for?'} - end - - def greet - standard_response("Hello #{params[:name]}!") - end - def wink - ok("I'm winking at you right now.") - end - def index - {:status => 200, :body => 'Wish you were cooler.'} - end - - user do - def show - {:status => 200, :body => "You request: #{params[:id]}"} - end - end - - def show - {:status => 200, :body => "This method does not conflict with the show method in the user module."} - end - - # custom 404 error handler - def what_are_you_looking_for? - raise Exceptions::NotFound.new(404, 'Not Found; You did not find what you were expecting because it is not here. What are you looking for?') - end -end -Rack::Handler::Mongrel.run Simple.new, :Port => 3801 if __FILE__ == $0 diff --git a/lib/halcyon.rb b/lib/halcyon.rb deleted file mode 100644 index c997e27..0000000 --- a/lib/halcyon.rb +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env ruby -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -$:.unshift File.dirname(__FILE__) - -#-- -# dependencies -#++ - -%w(rubygems merb/core_ext).each {|dep|require dep} - -#-- -# module -#++ - -module Halcyon - VERSION = [0,4,0] - def self.version - VERSION.join('.') - end - - # = Introduction - # - # Halcyon is a JSON Web Server Framework intended to be used for fast, small - # data transactions, like for AJAX-intensive sites or for special services like - # authentication centralized for numerous web apps in the same cluster. - # - # The possibilities are pretty limitless: the goal of Halcyon was simply to be - # lightweight, fast, simple to implement and use, and able to be extended. - # - # == Usage - # - # For documentation on using Halcyon, check out the Halcyon::Server::Base and - # Halcyon::Client::Base classes which contain much more usage documentation. - def introduction - abort "READ THE DAMNED RDOCS!" - end - - #-- - # Module Autoloading - #++ - - class Server - module Auth - autoload :Basic, 'halcyon/server/auth/basic' - end - end - -end - -%w(halcyon/exceptions).each {|dep|require dep} diff --git a/lib/halcyon/client.rb b/lib/halcyon/client.rb deleted file mode 100644 index 8f55872..0000000 --- a/lib/halcyon/client.rb +++ /dev/null @@ -1,49 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -$:.unshift File.dirname(File.join('..', __FILE__)) -$:.unshift File.dirname(__FILE__) - -#-- -# dependencies -#++ - -%w(rubygems halcyon).each {|dep|require dep} -begin - require 'json/ext' -rescue LoadError => e - warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.' - require 'json/pure' -end - -#-- -# module -#++ - -module Halcyon - - # The Client library provides a simple way to package up a client lib to - # simplify communicating with the accompanying Halcyon server app. - # - # = Usage - # - # For documentation on using Halcyon, check out the Halcyon::Server::Base and - # Halcyon::Client::Base classes which contain much more usage documentation. - class Client - def self.version - VERSION.join('.') - end - - #-- - # module dependencies - #++ - - autoload :Base, 'halcyon/client/base' - autoload :Router, 'halcyon/client/router' - - end -end - -%w(halcyon/client/exceptions).each {|dep|require dep} diff --git a/lib/halcyon/client/base.rb b/lib/halcyon/client/base.rb deleted file mode 100644 index e319e00..0000000 --- a/lib/halcyon/client/base.rb +++ /dev/null @@ -1,261 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# dependencies -#++ - -%w(net/http uri json).each {|dep|require dep} - -#-- -# module -#++ - -module Halcyon - class Client - - DEFAULT_OPTIONS = {} - USER_AGENT = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon/#{Halcyon.version} Client/#{Halcyon::Client.version}" - CONTENT_TYPE = 'application/json' - - # = Building Custom Clients - # - # Once your Halcyon JSON Server App starts to take shape, it may be useful - # to begin to write tests on expected functionality, and then to implement - # API calls with a designated Client lib for your Ruby or Rails apps, etc. - # The Base class provides a standard implementation and several options for - # wrapping up functionality of your app from the server side into the - # client side so that you may begin to use response data. - # - # == Creating Your Client - # - # Creating a simple client can be as simple as this: - # - # class Simple < Halcyon::Client::Base - # def greet(name) - # get("/hello/#{name}") - # end - # end - # - # The only thing simply may be actually using the Simple client you just - # created. - # - # But to actually get in and use the library, one has to take full - # advantage of the HTTP request methods, +get+, +post+, +put+, and - # +delete+. These methods simply return the JSON-parsed data from the - # server, effectively returning a hash with two key values, +status+ which - # contains the HTTP status code, and +body+ which contains the body of the - # content returned which can be any number of objects, including, but not - # limited to Hash, Array, Numeric, Nil, Boolean, String, etc. - # - # You are not limited to what your methods can call: they are arbitrarily - # and solely up to your whims and needs. It is simply a matter of good - # design and performance when it comes to structuring and implementing - # client actions which can be complex or simple series of requests to the - # server. - # - # == Acceptable Clients - # - # The Halcyon Server is intended to be very picky with whom it will speak - # to, so it requires that we specifically mention that we speak only - # "application/html", that we're "JSON/1.1.1 Compatible", and that we're - # local to the server itself (in process, anyways). This ensures that it - # has to deal with as little noise as possible and focus it's attention on - # performing our requests. - # - # This shouldn't affect usage when working with the Client or in production - # but might if you're trying to check things in your browser. Just make - # certain that the debug option is turned on (-d for the +halcyon+ command) - # when you start the server so that it knows to be a little more lenient - # about to whom it speaks. - class Base - - #-- - # Initialization and setup - #++ - - # = Connecting to the Server - # - # Creates a new Client object to allow for requests and responses from - # the specified server. - # - # The +uri+ param contains the URL to the actual server, and should be in - # the format: "http://localhost:3801" or "http://app.domain.com:3401/" - # - # == Server Connections - # - # Connecting only occurs at the actual event that a request is performed, - # so there is no need to worry about closing connections or managing - # connections in general other than good object housecleaning. (Be nice - # to your Garbage Collector.) - # - # == Usage - # - # You can either provide a block to perform all of your requests and - # processing inside of or you can simply accept the object in response - # and call your request methods off of the returned object. - # - # Alternatively, you could do both. - # - # An example of creating and using a Simple client: - # - # class Simple < Halcyon::Client::Base - # def greet(name) - # get("/hello/#{name}") - # end - # end - # Simple.new('http://localhost:3801') do |s| - # puts s.greet("Johnny").inspect - # end - # - # This should effectively call +inspect+ on a response hash similar to - # this: - # - # {:status => 200, :body => 'Hello Johnny'} - # - # Alternatively, you could perform the same with the following: - # - # s = Simple.new('http://localhost:3801') - # puts s.greet("Johnny").inspect - # - # This should generate the exact same outcome as the previous example, - # except that it is not executed in a block. - # - # The differences are purely semantic and of personal taste. - def initialize(uri) - @uri = URI.parse(uri) - if block_given? - yield self - end - end - - #-- - # Reverse Routing - #++ - - # = Reverse Routing - # - # The concept of writing our Routes in our Client is to be able to - # automatically generate the appropriate URL based on the hash given - # and where it was called from. This makes writing actions in Clients - # go from something like this: - # - # def greet(name) - # get("/hello/#{name}") - # end - # - # to this: - # - # def greet(name) - # get(url_for(__method__, :name)) - # end - # - # This doesn't immediately seem to be beneficial, but it is better for - # automating URL generating, taking out the hardcoding, and has room to - # to improve in the future. - def url_for(action, params = {}) - Halcyon::Client::Router.route(action, params) - end - - # Sets up routing for creating preparing +url_for+ URLs. See the - # +url_for+ method documentation and the Halcyon::Client::Router docs. - def self.route - if block_given? - Halcyon::Client::Router.prepare do |r| - Halcyon::Client::Router.default_to yield(r) - end - else - warn "Routes should be defined in a block." - end - end - - #-- - # Request Handling - #++ - - # Performs a GET request on the URI specified. - def get(uri, headers={}) - req = Net::HTTP::Get.new(uri) - request(req, headers) - end - - # Performs a POST request on the URI specified. - def post(uri, data, headers={}) - req = Net::HTTP::Post.new(uri) - req.body = format_body(data) - request(req, headers) - end - - # Performs a DELETE request on the URI specified. - def delete(uri, headers={}) - req = Net::HTTP::Delete.new(uri) - request(req, headers) - end - - # Performs a PUT request on the URI specified. - def put(uri, data, headers={}) - req = Net::HTTP::Put.new(uri) - req.body = format_body(data) - request(req, headers) - end - - private - - # Performs an arbitrary HTTP request, receive the response, parse it with - # JSON, and return it to the caller. This is a private method because the - # user/developer should be quite satisfied with the +get+, +post+, +put+, - # and +delete+ methods. - # - # == Request Failures - # - # If the server responds with any kind of failure (anything with a status - # that isn't 200), Halcyon will in turn raise the respective exception - # (defined in Halcyon::Exceptions) which all inherit from - # +Halcyon::Exceptions::Base+. It is up to the client to handle these - # exceptions specifically. - def request(req, headers={}) - # define essential headers for Halcyon::Server's picky requirements - req["Content-Type"] = CONTENT_TYPE - req["User-Agent"] = USER_AGENT - - # apply provided headers - headers.each do |pair| - header, value = pair - req[header] = value - end - - # provide hook for modifying the headers - req = headers(req) if respond_to? :headers - - # prepare and send HTTP request - res = Net::HTTP.start(@uri.host, @uri.port) {|http|http.request(req)} - - # parse response - body = JSON.parse(res.body) - body.symbolize_keys! - - # handle non-successes - raise Halcyon::Client::Base::Exceptions.lookup(body[:status]).new unless res.kind_of? Net::HTTPSuccess - - # return response - body - rescue Halcyon::Exceptions::Base => e - # log exception if logger is in place - raise - end - - # Formats the data of a POST or PUT request (the body) into an acceptable - # format according to Net::HTTP for sending through as a Hash. - def format_body(data) - data = {:body => data} unless data.is_a? Hash - data.symbolize_keys! - # uses the Merb Hash#to_params method defined in merb/core_ext. - data.to_params - end - - end - - end -end diff --git a/lib/halcyon/client/exceptions.rb b/lib/halcyon/client/exceptions.rb deleted file mode 100644 index 064635b..0000000 --- a/lib/halcyon/client/exceptions.rb +++ /dev/null @@ -1,41 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# module -#++ - -module Halcyon - class Client - class Base - module Exceptions #:nodoc: - - #-- - # Exception classes - #++ - - Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error| - status, body = http_error - class_eval( - "class #{body.gsub(/( |\-)/,'')} < Halcyon::Exceptions::Base\n"+ - " def initialize(s=#{status}, e='#{body}')\n"+ - " super\n"+ - " end\n"+ - "end" - ); - end - - #-- - # Exception Lookup - #++ - - def self.lookup(status) - self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,'')) - end - - end - end - end -end diff --git a/lib/halcyon/client/router.rb b/lib/halcyon/client/router.rb deleted file mode 100644 index 3f879b8..0000000 --- a/lib/halcyon/client/router.rb +++ /dev/null @@ -1,106 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# module -#++ - -module Halcyon - class Client - - # = Reverse Routing - # - # Handles URL generation from route params and action names to complement - # the routing ability in the Server. - # - # == Usage - # - # class Simple < Halcyon::Client::Base - # route do |r| - # r.match('/path/to/match').to(:action => 'do_stuff') - # {:action => 'not_found'} # the default route - # end - # def greet(name) - # get(url_for(__method__, :name => name)) - # end - # end - # - # == Default Routes - # - # The default route is selected if and only if no other routes matched the - # action and params supplied as a fallback query to supply. This should - # generate an error in most cases, unless you plan to handle this exception - # specifically. - class Router - - # Retrieves the last value from the +route+ call in Halcyon::Client::Base - # and, if it's a Hash, sets it to +@@default_route+ to designate the - # failover route. If +route+ is not a Hash, though, the internal default - # should be used instead (as the last returned value is probably a Route - # object returned by the +r.match().to()+ call). - # - # Used exclusively internally. - def self.default_to route - @@default_route = route.is_a?(Hash) ? route : {:action => 'not_found'} - end - - # This method performs the param matching and URL generation based on the - # inputs from the +url_for+ method. (Caution: not for the feint hearted.) - def self.route(action, params) - r = nil - @@routes.each do |r| - path, pars = r - if pars[:action] == action - # if the actions match up (a pretty good sign of success), make sure the params match up - if (!pars.empty? && !params.empty? && (/(:#{params.keys.first})/ =~ path).nil?) || - ((pars.empty? && !params.empty?) || (!pars.empty? && params.empty?)) - r = nil - next - else - break - end - end - end - - # make sure a route is returned even if no match is found - if r.nil? - #return default route - @@default_route - else - # params (including action and module if set) for the matching route - path = r[0].dup - # replace all params with the proper placeholder in the path - params.each{|p| path.gsub!(/:#{p[0]}/, p[1]) } - path - end - end - - #-- - # Route building methods - #++ - - # Sets up the +@@routes+ hash and begins the processing by yielding to the block. - def self.prepare - @@path = nil - @@routes = {} - yield self if block_given? - end - - # Stores the path temporarily in order to put it in the hash table. - def self.match(path) - @@path = path - self - end - - # Adds the final route to the hash table and clears the temporary value. - def self.to(params={}) - @@routes[@@path] = params - @@path = nil - self - end - - end - end -end diff --git a/lib/halcyon/exceptions.rb b/lib/halcyon/exceptions.rb deleted file mode 100644 index 5b9f56d..0000000 --- a/lib/halcyon/exceptions.rb +++ /dev/null @@ -1,98 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# module -#++ - -module Halcyon - module Exceptions #:nodoc: - - #-- - # Base Halcyon Exception - #++ - - class Base < StandardError #:nodoc: - attr_accessor :status, :error - def initialize(status, error) - @status = status - @error = error - super "[#{@status}] #{@error}" - end - end - - #-- - # HTTP Error Codes and Errors - #++ - - HTTP_ERROR_CODES = { - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported' - } - end -end - -# Taken from Rack's definition: -# http://chneukirchen.org/darcs/darcsweb.cgi?r=rack;a=plainblob;f=/lib/rack/utils.rb -# -# HTTP_STATUS_CODES = { -# 100 => 'Continue', -# 101 => 'Switching Protocols', -# 200 => 'OK', -# 201 => 'Created', -# 202 => 'Accepted', -# 203 => 'Non-Authoritative Information', -# 204 => 'No Content', -# 205 => 'Reset Content', -# 206 => 'Partial Content', -# 300 => 'Multiple Choices', -# 301 => 'Moved Permanently', -# 302 => 'Moved Temporarily', -# 303 => 'See Other', -# 304 => 'Not Modified', -# 305 => 'Use Proxy', -# 400 => 'Bad Request', -# 401 => 'Unauthorized', -# 402 => 'Payment Required', -# 403 => 'Forbidden', -# 404 => 'Not Found', -# 405 => 'Method Not Allowed', -# 406 => 'Not Acceptable', -# 407 => 'Proxy Authentication Required', -# 408 => 'Request Time-out', -# 409 => 'Conflict', -# 410 => 'Gone', -# 411 => 'Length Required', -# 412 => 'Precondition Failed', -# 413 => 'Request Entity Too Large', -# 414 => 'Request-URI Too Large', -# 415 => 'Unsupported Media Type', -# 500 => 'Internal Server Error', -# 501 => 'Not Implemented', -# 502 => 'Bad Gateway', -# 503 => 'Service Unavailable', -# 504 => 'Gateway Time-out', -# 505 => 'HTTP Version not supported' -# } diff --git a/lib/halcyon/server.rb b/lib/halcyon/server.rb deleted file mode 100644 index 58d00c4..0000000 --- a/lib/halcyon/server.rb +++ /dev/null @@ -1,62 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -$:.unshift File.dirname(File.join('..', __FILE__)) -$:.unshift File.dirname(__FILE__) - -#-- -# dependencies -#++ - -%w(rubygems halcyon rack).each {|dep|require dep} -begin - require 'json/ext' -rescue LoadError => e - warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.' - require 'json/pure' -end - -#-- -# module -#++ - -module Halcyon - - # = Server Communication and Protocol - # - # Server tries to comply with appropriate HTTP response codes, as found at - # . However, all - # responses are JSON encoded as the server expects a JSON parser on the - # client side since the server should not be processing requests directly - # through the browser. The server expects the User-Agent to be one of: - # +"User-Agent" => "JSON/1.1.1 Compatible (en-US) Halcyon/0.0.12 Client/0.0.1"+ - # +"User-Agent" => "JSON/1.1.1 Compatible"+ - # The server also expects to accept application/json and be originated - # from the local host (though this can be overridden). - # - # = Usage - # - # For documentation on using Halcyon, check out the Halcyon::Server::Base and - # Halcyon::Client::Base classes which contain much more usage documentation. - class Server - def self.version - VERSION.join('.') - end - - #-- - # module dependencies - #++ - - autoload :Base, 'halcyon/server/base' - autoload :Router, 'halcyon/server/router' - - end - -end - -# Loads the Exceptions class first which sets up all the dynamically generated -# exceptions used by the system. Must occur before Base is loaded since Base -# depends on it. -%w(halcyon/server/exceptions).each {|dep|require dep} diff --git a/lib/halcyon/server/auth/basic.rb b/lib/halcyon/server/auth/basic.rb deleted file mode 100644 index 8640700..0000000 --- a/lib/halcyon/server/auth/basic.rb +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env ruby -#-- -# Created by Matt Todd on 2008-01-16. -# Copyright (c) 2008. All rights reserved. -#++ - -#-- -# module -#++ - -module Halcyon - class Server - module Auth - - # = Introduction - # - # The Auth::Basic class provides an alternative to the Server::Base - # class for creating servers with HTTP Basic Authentication built in. - # - # == Usage - # - # In order to provide for HTTP Basic Authentication in your server, - # it would first need to inherit from this class instead of Server::Base - # and then provide a method to check for the existence of the credentials - # and respond accordingly. This looks like the following: - # - # class AuthenticatedApp < Halcyon::Server::Auth::Basic - # def basic_authorization(username, password) - # [username, password] == ['rupert', 'secret'] - # end - # # write normal Halcyon server app here - # end - # - # The credentials passed to the +basic_authorization+ method are pulled - # from the appropriate Authorization header value and parsed from the - # base64 values. If no Authorization header value is passed, an exception - # is thrown resulting in the appropriate response to the client. - class Basic < Server::Base - - AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION'] - - # Determines the appropriate HTTP Authorization header to refer to when - # plucking out the header for processing. - def authorization_key - @authorization_key ||= AUTHORIZATION_KEYS.detect{|k|@env.has_key?(k)} - end - - alias :_run :run - - # Ensures that the HTTP Authentication header is included, the Basic - # scheme is being used, and the credentials pass the +basic_auth+ - # test. If any of these fail, an Unauthorized exception is raised - # (except for non-Basic schemes), otherwise the +route+ is +run+ - # normally. - # - # See the documentation for the +basic_auth+ class method for details - # concerning the credentials and action inclusion/exclusion. - def run(route) - # test credentials if the action is one specified to be tested - if ((@@auth[:except].nil? && @@auth[:only].nil?) || # the default is to test if no restrictions - (!@@auth[:only].nil? && @@auth[:only].include?(route[:action].to_sym)) || # but if the action is in the :only directive, test - (!@@auth[:except].nil? && !@@auth[:except].include?(route[:action].to_sym))) # or if the action is not in the :except directive, test - - # make sure there's an authorization header - raise Base::Exceptions::Unauthorized.new unless !authorization_key.nil? - - # make sure the request is via the Basic protocol - scheme = @env[authorization_key].split.first.downcase.to_sym - raise Base::Exceptions::BadRequest.new unless scheme == :basic - - # make sure the credentials pass the test - credentials = @env[authorization_key].split.last.unpack("m*").first.split(':', 2) - raise Base::Exceptions::Unauthorized.new unless @@auth[:method].call(*credentials) - end - - # success, so run the route normally - _run(route) - rescue Halcyon::Exceptions::Base => e - @logger.warn "#{uri} => #{e.error}" - # handles all content error exceptions - @res.status = e.status - {:status => e.status, :body => e.error} - end - - # Provides a way to define a test as well as set limits on what is - # tested for Basic Authorization. This method should be called in the - # definition of the server. A simple example would look like: - # - # class Servr < Halcyon::Server::Auth::Basic - # basic_auth :only => [:grant] do |user, pass| - # # test credentials - # end - # # routes and actions follow... - # end - # - # Two acceptable options include :only and :except. - def self.basic_auth(options={}, &proc) - instance_eval do - @@auth = options.merge(:method => proc) - end - end - - end - - end - end -end diff --git a/lib/halcyon/server/base.rb b/lib/halcyon/server/base.rb deleted file mode 100644 index 293db8d..0000000 --- a/lib/halcyon/server/base.rb +++ /dev/null @@ -1,777 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# dependencies -#++ - -%w(logger json).each {|dep|require dep} - -#-- -# module -#++ - -module Halcyon - class Server - - DEFAULT_OPTIONS = { - :root => Dir.pwd, - :environment => 'none', - :port => 9267, - :host => 'localhost', - :server => Gem.searcher.find('thin').nil? ? 'mongrel' : 'thin', - :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid', - :log_file => '/var/log/halcyon.{app}.log', - :log_level => 'info', - :log_format => proc{|s,t,p,m|"#{s} [#{t.strftime("%Y-%m-%d %H:%M:%S")}] (#{$$}) #{p} :: #{m}\n"}, - # handled internally - :acceptable_requests => [], - :acceptable_remotes => [] - } - ACCEPTABLE_REQUESTS = [ - # ENV var to check, Regexp the value should match, the status code to return in case of failure, the message with the code - ["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'], - ["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type'] - ] - ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1', '0.0.0.0'] - - # = Building Halcyon Server Apps - # - # Halcyon apps are actually little servers running on top of Rack instances - # which affords a great deal of simplicity and quickness to both design and - # performance. - # - # Building a Halcyon app consists of defining routes to map all requests - # against in order to designate what functionality handles what specific - # requests, the actual actions (and modules) to actually perform these - # requests, and any extensions or configurations you may need or want for - # your individual needs. - # - # == Inheriting from Halcyon::Server::Base - # - # To begin with, an application would be started by simply defining a class - # that inherits from Halcyon::Server::Base. - # - # class Greeter < Halcyon::Server::Base - # end - # - # Once this task has been completed, routes can be defined. - # - # class Greeter < Halcyon::Server::Base - # route do |r| - # r.match('/hello/:name').to(:action => 'greet') - # {:action => 'not_found'} # default route - # end - # end - # - # Two routes are (effectively) defined here, the first being to watch for - # all requests in the format /hello/:name where the word pattern - # is stored and transmitted as the appropriate keys in the params hash. - # - # Once we've got our inputs specified, we can start to handle requests: - # - # class Greeter < Halcyon::Server::Base - # route do |r| - # r.match('/hello/:name').to(:action => 'greet') - # {:action => 'not_found'} # default route - # end - # def greet; {:status=>200, :body=>"Hi #{params[:name]}"}; end - # end - # - # You will notice that we only define the method +greet+ and that it - # returns a Hash object containing a +status+ code and +body+ content. - # This is the most basic way to send data, but if all you're doing is - # replying that the request was successful and you have data to return, - # the method +ok+ (an alias of standard_response) with the +body+ - # param as its sole parameter is sufficient. - # - # - # def greet; ok("Hi #{params[:name]}"); end - # - # You'll also notice that there's no method called +not_found+; this is - # because it is already defined and behaves almost exactly like the +ok+ - # method. We could certainly overwrite +not_found+, but at this point it - # is not necessary. - # - # You should also realize that the second route is not defined. This is - # classified as the default route, the route to follow in the event that no - # route actually matches, so it doesn't need any of the extra path to match - # against. - # - # Lastly, the use of +params+ inside the method is simply a method call - # to a hash of the parameters gleaned from the route, such as +:name+ or - # any other variables passed to it. - # - # == The Filesystem - # - # It's important to note that the +halcyon+ commandline tool expects to - # find your server inheriting +Halcyon::Server::Base+ with the same exact - # name as its filename, though with special rules. - # - # To clarify, when your server is stored in +app_server.rb+, it expects - # that your server's class name be +AppServer+ as it capitalizes each word - # and removes all underscores, etc. - # - # Keep this in mind when naming your class and your file, though this - # restriction is only temporary. - # - # NOTE: This really isn't a necessary step if you write your own deployment - # script instead of using the +halcyon+ commandline tool (as it is simply - # a convenience tool). In such, feel free to name your server however you - # prefer and the file likewise. - # - # == Running Your Server On Your Own - # - # If you're wanting to run your server without the help of the +halcyon+ - # commandline tool, you will simply need to initialize the server as you - # pass it to the Rack handler of choice along with any configuration - # options you desire. - # - # The following should be enough: - # - # Rack::Handler::Mongrel.run YourAppName.new(options), :Port => 9267 - # - # Of course Halcyon already handles most of your dependencies for you, so - # don't worry about requiring Rack, et al. And again, the options are not - # mandatory as the default options are certainly acceptable. - # - # NOTE: If you want to provide debugging information, just set +$debug+ to - # +true+ and you should receive all the debugging information available. - class Base - - #-- - # Request Handling - #++ - - # = Handling Calls - # - # Receives the request, handles the route matching, runs the approriate - # action based on the route determined (or defaulted to) and finishes by - # responding to the client with the content returned. - # - # == Response and Output - # - # Halcyon responds in purely JSON format (except perhaps on sever server - # malfunctions that aren't caught or intended; read: bugs). - # - # The standard response is simply a JSON-encoded hash following this - # format: - # - # {:status => http_status_code, :body => response_body} - # - # Response body can be any object desired (as long as there is a - # +to_json+ method for it, which includes most core classes), usually - # containing a nested hash with appropriate data. - # - # DO NOT try to call +to_json+ on the +body+ contents as this will cause - # errors when trying to parse JSON. - # - # == Request and Response - # - # If you need access to the Request and Response, the instance variables - # +@req+ and +@res+ will be sufficient for you. - # - # If you need specific documentation for these objects, check the - # corresponding docs in the Rack documentation. - # - # == Requests and POST Data - # - # Most of your requests will have all the data it needs inside of the - # +params+ you receive for your action, but for POST and PUT requests - # (you are being RESTful, right?) you will need to retrieve your data - # from the method +post+. Here's how: - # - # post[:key] => "value" - # - # As you can see, keys specifically are symbols and values as well. What - # this means is that your POST data that you send to the server needs to - # be careful to provide a flat Hash (if anything other than a Hash is - # passed, it is packed up into a hash similar to +{:body=>data}+) or at - # least send a complicated structure as a JSON object so that transport - # is clean. Resurrecting the object is still on your end for POST data - # (though this could change). Here's how you would reconstruct your - # special hash: - # - # value = JSON.parse(post[:key]) - # - # That will take care of reconstructing your Hash. - # - # And that is essentially all you need to worry about for retreiving your - # POST contents. Sending POST contents should be documented well enough - # in Halcyon::Client::Base. - # - # == Logging - # - # Logging can be done by logging to +@logger+ when inside the scope of - # application instance (inside of your instance methods and modules). - # - # The +@env+ instance variable has been modified to include a - # +halcyon.logger+ property including the given logger. Use this for - # logging if you need to step outside of the scope of the current - # application instance (just be sure to pass @env along with you). - def call(env) - @time_started = Time.now - - # collect env information, create request and response objects, prep for dispatch - # puts env.inspect if $debug # request information (huge) - @env = env - @res = Rack::Response.new - @req = Rack::Request.new(env) - - # set the User Agent (to be nice to anything to needs accurate information from it) - @res['User-Agent'] = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon::Server/#{Halcyon.version}" - - # add the logger to the @env instance variable for global access if for - # some reason the environment needs to be passed outside of the - # instance - @env['halcyon.logger'] = @logger - - # pre run hook - before_run(Time.now - @time_started) if respond_to? :before_run - - # prepare route and provide it for callers - route = Router.route(@env) - @env['halcyon.route'] = route - - # dispatch - @res.write(run(route).to_json) - - # post run hook - after_run(Time.now - @time_started) if respond_to? :after_run - - @time_finished = Time.now - @time_started - - # logs access in the following format: [200] / => index (0.0029s;343.79req/s) - req_time, req_per_sec = ((@time_finished*1e4).round.to_f/1e4), (((1.0/@time_finished)*1e2).round.to_f/1e2) - @logger.info "[#{@res.status}] #{@env['REQUEST_URI']} => #{route[:module].to_s}#{((route[:module].nil?) ? "" : "::")}#{route[:action]} (#{req_time}s;#{req_per_sec}req/s)" - - # finish request - @res.finish - end - - # = Dispatching Requests - # - # Dispatches the routed request, handling module resolution and pulling - # all of the param values together for the action. This action is called - # by +call+ and should be transparent to your server app. - # - # One of the design elements of this method is that it rescues all - # Halcon-specific exceptions (defined innside of ::Base::Exceptions) so - # that a proper JSON response may be rendered by +call+. - # - # With this in mind, it is preferred that, for any errors that should - # result in a given HTTP Response code other than 2xx, an appropriate - # exception should be thrown which is then handled by this method's - # rescue clause. - # - # Refer to the Exceptions module to see a list of available Exceptions. - # - # == Acceptable Requests - # - # Halcyon is a very picky server when dealing with requests, requiring - # that clients match a given remote location, accepting JSON responses, - # and matching a certain User-Agent profile. Unless running in debug - # mode, Halcyon will reject all requests with a 403 Forbidden response - # if these requirements are not met. - # - # This means, while in development and testing, the debug flag must be - # enabled if you intend to perform initial tests through the browser. - # - # These restrictions may appear to be arbitrary, but it is simply a - # measure to prevent a live server running in production mode from being - # assaulted by unacceptable clients which keeps the server performing - # actual functions without concerning itself with non-acceptable clients. - # - # The requirements are defined by the Halcyon::Server constants: - # * +ACCEPTABLE_REQUESTS+: defines the necessary User-Agent and Accept - # headers the client must provide. - # * ACCEPTABLE_REMOTES: defines the acceptable remote origins of - # any request. This is primarily limited to - # only local requests, but can be changed. - # - # Halcyon servers are intended to be run behind other applications and - # primarily only speaking with other apps on the same machine, though - # your specific requirements may differ and change that. - # - # When in debug mode or in testing mode, the request filtering test is - # not fired, so all requests from all User Agents and locations will - # succeed. This is important to know if you plan on testing this specific - # feature while in debugging or testing modes. - # - # == Hooks, Callbacks, and Authentication - # - # There is no Authentication mechanism built in to Halcyon (for the time - # being), but there are hooks and callbacks for you to be able to ensure - # that requests are authenticated, etc. - # - # In order to set up a callback, simply define one of the following - # methods in your app's base class: - # * before_run - # * before_action - # * after_action - # * after_run - # - # This is the exact order in which the callbacks are performed if - # defined. Make use of these methods to monitor incoming and outgoing - # requests. - # - # It is preferred for these methods to throw Exceptions::Base exceptions - # (or one of its inheriters) instead of handling them manually. This - # ensures that the actual action is not run when in fact it shouldn't, - # otherwise you could be allowing unauthenticated users privileged - # information or allowing them to perform destructive actions. - def run(route) - # make sure the request meets our expectations - acceptable_request! unless $debug || $test - - # pull params - @params = route.reject{|key, val| [:action, :module].include? key} - @params.merge!(query_params) - - # pre call hook - before_call if respond_to? :before_call - - # handle module actions differently than non-module actions - if route[:module].nil? - # call action - res = send(route[:action]) - else - # call module action - mod = self.dup - mod.instance_eval(&(@@modules[route[:module].to_sym])) - res = mod.send(route[:action]) - end - - # after call hook - after_call if respond_to? :after_call - - @params = {} - - res - rescue Halcyon::Exceptions::Base => e - @logger.warn "#{uri} => #{e.error}" - # handles all content error exceptions - @res.status = e.status - {:status => e.status, :body => e.error} - end - - # Tests for acceptable requests if +$debug+ and +$test+ are not set. - def acceptable_request! - @config[:acceptable_requests].each do |req| - raise Halcyon::Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1] - end - raise Exceptions::Forbidden.new unless @config[:acceptable_remotes].member? @env["REMOTE_ADDR"] - end - - #-- - # Initialization and setup - #++ - - # Called when the Handler gets started and stores the configuration - # options used to start the server. - # - # Feel free to define initialize for your app (which is only called once - # per server instance), just be sure to call +super+. - # - # == PID File - # - # A PID file is created when the server is first initialized with the - # current process ID. Where it is located depends on the default option, - # the config file, the commandline option, and the debug status, - # increasing in precedence in that order. - # - # By default, the PID file is placed in +/var/run/+ and is named - # +halcyon.{server}.{app}.{port}.pid+ where +{server}+ is replaced by the - # running server, +{app}+ is the app name (suffixed with +#debug+ if - # running in debug mode), and +{port}+ being the server port (if there - # are multiple servers running, this helps clarify). - # - # There is an option to numerically label your server via the +{n}+ - # value, but this is deprecated and will be removed soon. Using the - # +{port}+ option makes much more sense and creates much more meaning. - def initialize(options = {}) - # save configuration options - @config = DEFAULT_OPTIONS.merge(options) - @config[:app] ||= self.class.to_s.downcase - - # apply name options to log_file and pid_file configs - apply_log_and_pid_file_name_options - - # debug and test mode handling - enable_debugging if $debug - enable_testing if $test - - # setup logging - setup_logging unless $debug || $test - - # setup request filtering - setup_request_filters unless $debug || $test - - # create PID file - @pid = File.new(@config[:pid_file].gsub('{n}', server_cluster_number), "w", 0644) - @pid << "#{$$}\n"; @pid.close - - # log existence - @logger.info "PID file created. PID is #{$$}." - - # call startup callback if defined - startup if respond_to? :startup - - # log ready state - @logger.info "Started. Awaiting connectivity. Listening on #{@config[:port]}..." - - # trap signals to die (when killed by the user) gracefully - finalize = Proc.new do - @logger.info "Shutting down #{$$}." - clean_up - exit - end - # http://en.wikipedia.org/wiki/Signal_%28computing%29 - %w(INT KILL TERM QUIT HUP).each{|sig|trap(sig, finalize)} - - # listen for USR1 signals and toggle debugging accordingly - trap("USR1") do - if $debug - disable_debugging - else - enable_debugging - end - end - end - - # Closes the logger and deletes the PID file. - def clean_up - # don't try to clean up what's cleaned up already - return if defined? @cleaned_up - - # run shutdown hook if defined - shutdown if respond_to? :shutdown - - # close logger, delete PID file, flag clean state - @logger.close - File.delete(@pid.path) if File.exist?(@pid.path) - @cleaned_up = true - end - - # Retreives the server cluster sequence number for the PID file. - # - # This is deprecated and will be removed soon, probably for the 0.4.0 - # release. Use of the +{port}+ value is much more appropriate and - # meaningful. - def server_cluster_number - # if there are no +{n}+ references in the PID file name, then simply - # return 0 as the cluster number. (This is the preferred behavior and - # this test allows the method to fail fast. +{n}+ is deprecated and - # will be removed before 0.4.0 is released.) - return 0.to_s if @config[:pid_file]['{n}'].nil? - - # warn users that they're using a deprecated convention. - warn "Your PID file name contains '{n}' (#{@config[:pid_file]}). This is deprecatd and will be removed by the 0.4.0 release. Use '{port}' instead." - - # counts the number of PID files already existing. - server_count = Dir[@config[:pid_file].gsub('{n}','*')].length - # since the counting starts at 0, if the file with the count exists, - # then one of the lesser number servers isn't running, so check each - # PID file until the one not running is found. - # if no files exist, then 0 will be the count, which won't exist, so - # it will be the default number. - while File.exist?(@config[:pid_file].gsub('{n}',server_count.to_s)) - server_count -= 1 - end - # return that number. - server_count.to_s - end - - # If the server receives a SIGUSR1 signal it will toggle debugging. This - # method is used to setup logging and the request handling methods for - # debugging. - def enable_debugging - $debug = true - - # set the PID file name to /tmp/ unless PID file already exists - @config[:pid_file] = '/tmp/halcyon.{server}.{app}.{port}.pid' unless defined? @pid - apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set - - # setup logger to STDOUT and log entering debugging mode - @logger = Logger.new(STDOUT) - @logger.progname = "#{self.class}#debug" - @logger.level = Logger::DEBUG - @logger.formatter = @config[:log_format] - @logger.info "Entering debugging mode..." - rescue Errno::EACCES - abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'" - end - - # This method is used to setup logging and the request handling methods - # for debugging. - def enable_testing - # set the PID file name to /tmp/ unless PID file already exists - @config[:pid_file] = '/tmp/halcyon.testing.{app}.{port}.pid' unless defined? @pid - @config[:log_file] = '/tmp/halcyon.testing.{app}.log' - apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set - - # setup logger and log entering testing mode - @logger = Logger.new(@config[:log_file]) - @logger.progname = "#{self.class}#test" - @logger.level = Logger::DEBUG - @logger.formatter = @config[:log_format] - @logger.info "Entering testing mode..." - - # make sure we clean up after ourselves since we're in testing mode - at_exit { - clean_up - File.delete(@config[:log_file]) if File.exist?(@config[:log_file]) - } - rescue Errno::EACCES - abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'" - end - - # Disables all of the affects of debugging mode and returns logging and - # request filtering back to normal. - # - # Refer to +enable_debugging+ for more information. - def disable_debugging - # disable logging and log leaving debugging mode - $debug = false - @logger.info "Leaving debugging mode." - - # setup normal logging - setup_logging - - # reenable request filtering - setup_request_filters - end - - # Sets up logging based on the configuration options in +@config+, which - # is set (in order of lowest to highest precedence) in the default - # options, in the configuration file provided, on the commandline, and - # debug mode options. - # - # == Levels - # - # The accepted level values are as follows: - # - # * debug - # * info - # * warn - # * error - # * fatal - # * unknown - # - # These are the exact way you can refer to the logger level you'd like to - # log at from all points of option specification (listed above in order - # of ascending precedence). - # - # If a bogus value is entered, a warning will be issued and the value - # will be defaulted to 'debug'. (So don't mess up.) - def setup_logging - # get the logging level based on the name supplied - level = { - 'debug' => Logger::DEBUG, - 'info' => Logger::INFO, - 'warn' => Logger::WARN, - 'error' => Logger::ERROR, - 'fatal' => Logger::FATAL, - 'unknown' => Logger::UNKNOWN # wtf? - }[@config[:log_level]] - if level.nil? - warn "Logging level specified not acceptable. Defaulting to 'debug'. Check the documentation for the acceptable values." - @config[:log_level] = 'debug' - level = Logger::DEBUG - end - - # setup the logger - @logger = Logger.new(@config[:log_file]) - @logger.progname = self.class - @logger.level = level - @logger.formatter = @config[:log_format] - rescue Errno::EACCES - abort "Can't access #{@config[:log_file]}, try 'sudo #{$0}'" - end - - # Sets up request filters based on User-Agent, Content-Type, and Remote - # IP/address values. - # - # Extracted from +initialize+ to reduce repetition. - def setup_request_filters - @config[:acceptable_requests] = ACCEPTABLE_REQUESTS - @config[:acceptable_remotes] = ACCEPTABLE_REMOTES - end - - # Searches through the PID file name and the Log file name stored in the - # +@config+ variable for +{server}+, +{app}+, and +{port}+ values and - # sets them accordingly. - def apply_log_and_pid_file_name_options - # DEFAULT :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid', - @config[:pid_file].gsub!('{server}', @config[:server]) - @config[:pid_file].gsub!('{port}', @config[:port].to_s) - @config[:pid_file].gsub!('{app}', File.basename(@config[:app])) - # DEFAULT :log_file => '/var/log/halcyon.{app}.log', - @config[:log_file].gsub!('{server}', @config[:server]) - @config[:log_file].gsub!('{port}', @config[:port].to_s) - @config[:log_file].gsub!('{app}', File.basename(@config[:app])) - end - - # = Routing - # - # Halcyon expects its apps to have routes set up inside of the base class - # (the class that inherits from Halcyon::Server::Base). Routes are - # defined identically to Merb's routes (since Halcyon Router inherits all - # its functionality directly from the Merb Router). - # - # == Usage - # - # A sample Halcyon application defining and handling Routes follows: - # - # class Simple < Halcyon::Server::Base - # route do |r| - # r.match('/user/show/:id').to(:module => 'user', :action => 'show') - # r.match('/hello/:name').to(:action => 'greet') - # r.match('/').to(:action => 'index') - # {:action => 'not_found'} # default route - # end - # user do - # def show(p); ok(p[:id]); end - # end - # def greet(p); ok("Hi #{p[:name]}"); end - # def index(p); ok("..."); end - # def not_found(p); super; end - # end - # - # In this example we define numerous routes for actions and even an - # action in the 'user' module as well as handling the event that no route - # was matched (thereby passing to not_found). - # - # == Modules - # - # A module is simply a named block that whose methods get executed as if - # they were in Base but without conflicting any methods with them, very - # similar to module in Ruby. All that is required to define a module is - # something like this: - # - # admin do - # def users; ok(...); end - # end - # - # This just needs to add one directive when defining what a given route - # maps to, such as: - # - # route do |r| - # r.map('/admin/users').to(:module => 'admin', :action => 'users') - # end - # - # or, alternatively, you can just map to: - # - # r.map('/:module/:action').to() - # - # though it may be better to just explicitly state the module (for - # resolving cleanly when someone starts entering garbage that matches - # incorrectly). - # - # == More Help - # - # In addition to this, you may also find some of the documentation for - # the Router class helpful. However, since the Router is pulled directly - # from Merb, you really should look at the documentation for Merb. You - # can find the documentation on Merb's website at: http://merbivore.com/ - def self.route - if block_given? - Router.prepare do |router| - Router.default_to yield(router) || {:action => 'not_found'} - end - else - abort "Halcyon::Server::Base.route expects a block to define routes." - end - end - - # Registers modules internally. (This is designed in a way to prevent - # method naming collisions inside and outside of modules.) - def self.method_missing(name, *params, &proc) - @@modules ||= {} - @@modules[name] = proc - end - - #-- - # Properties and shortcuts - #++ - - # Takes +msg+ as parameter and formats it into the standard response type - # expected by an action's caller. This format is as follows: - # - # {:status => http_status_code, :body => json_encoded_body} - # - # The methods +standard_response+, +success+, and +ok+ all handle any - # textual message and puts it in the body field, defaulting to the 200 - # response class status code. - def standard_response(body = 'OK') - {:status => 200, :body => body} - end - alias_method :success, :standard_response - alias_method :ok, :standard_response - - # Similar to the +standard_response+ method, takes input and responds - # accordingly, which is by raising an exception (which handles formatting - # the response in the normal response hash). - def not_found(body = 'Not Found') - body = 'Not Found' if body.is_a?(Hash) && body.empty? - raise Exceptions::NotFound.new(404, body) - end - - # Returns the params of the current request, set in the +run+ method. - def params - @params - end - - # Returns the params following the ? in a given URL as a hash - def query_params - @env['QUERY_STRING'].split(/&/).inject({}){|h,kp| k,v = kp.split(/=/); h[k] = v; h}.symbolize_keys! - end - - # Returns the URI requested - def uri - # special parsing is done to remove the protocol, host, and port that - # some Handlers leave in there. (Fixes inconsistencies.) - URI.parse(@env['REQUEST_URI'] || @env['PATH_INFO']).path - end - - # Returns the Request Method as a lowercase symbol. - # - # One useful situation for this method would be similar to this: - # - # case method - # when :get - # # perform reading operations - # when :post - # # perform updating operations - # when :put - # # perform creating operations - # when :delete - # # perform deleting options - # end - # - # It can also be used in many other cases, like throwing an exception if - # an action is called with an unexpected method. - def method - @env['REQUEST_METHOD'].downcase.to_sym - end - - # Returns the POST data hash, making the keys symbols first. - # - # Use like post[:post_param]. - def post - @req.POST.symbolize_keys! - end - - # Returns the GET data hash, making the keys symbols first. - # - # Use like get[:get_param]. - def get - @req.GET.symbolize_keys! - end - - end - - end -end diff --git a/lib/halcyon/server/exceptions.rb b/lib/halcyon/server/exceptions.rb deleted file mode 100644 index a3d74ad..0000000 --- a/lib/halcyon/server/exceptions.rb +++ /dev/null @@ -1,41 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# module -#++ - -module Halcyon - class Server - class Base - module Exceptions #:nodoc: - - #-- - # Exception classes - #++ - - Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error| - status, body = http_error - class_eval( - "class #{body.gsub(/( |\-)/,'')} < Halcyon::Exceptions::Base\n"+ - " def initialize(s=#{status}, e='#{body}')\n"+ - " super\n"+ - " end\n"+ - "end" - ); - end - - #-- - # Exception Lookup - #++ - - def self.lookup(status) - self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,'')) - end - - end - end - end -end diff --git a/lib/halcyon/server/router.rb b/lib/halcyon/server/router.rb deleted file mode 100644 index 8ff6956..0000000 --- a/lib/halcyon/server/router.rb +++ /dev/null @@ -1,103 +0,0 @@ -#-- -# Created by Matt Todd on 2007-12-14. -# Copyright (c) 2007. All rights reserved. -#++ - -#-- -# dependencies -#++ - -begin - %w(rubygems merb/core_ext merb/router uri).each {|dep|require dep} -rescue LoadError => e - abort "Merb must be installed for Routing to function. Please install Merb." -end - -#-- -# module -#++ - -module Halcyon - class Server - - # = Routing - # - # Handles routing. - # - # == Usage - # - # class Xy < Halcyon::Server::Base - # route do |r| - # r.match('/path/to/match').to(:action => 'do_stuff') - # {:action => 'not_found'} # the default route - # end - # def do_stuff(params) - # [200, {}, 'OK'] - # end - # end - # - # == Default Routes - # - # Supplying a default route if none of the others match is good practice, - # but is unnecessary as the predefined route is always, automatically, - # going to contain a redirection to the +not_found+ method which already - # exists in Halcyon::Server::Base. This method is freely overwritable, and - # is recommended for those that wish to handle unroutable requests - # themselves. - # - # In order to set a different default route, simply end the call to +route+ - # with a hash containing the action (and optionally the module) to run. - # - # == The Hard Work - # - # The mechanics of the router are solely from the efforts of the Merb - # community. This functionality is completely ripped right out of Merb - # and makes it functional. All credit to them, and be sure to check out - # their great framework: if Halcyon isn't quite what you need, maybe Merb - # is. - # - # http://merbivore.com/ - class Router < Merb::Router - - # Retrieves the last value from the +route+ call in Halcyon::Server::Base - # and, if it's a Hash, sets it to +@@default_route+ to designate the - # failover route. If +route+ is not a Hash, though, the internal default - # should be used instead (as the last returned value is probably a Route - # object returned by the +r.match().to()+ call). - # - # Used exclusively internally. - def self.default_to route - @@default_route = route.is_a?(Hash) ? route : {:action => 'not_found'} - end - - # Called internally by the Halcyon::Server::Base#call method to match - # the current request against the currently defined routes. Returns the - # params list defined in the +to+ routing definition, opting for the - # default route if no match is made. - def self.route(env) - # pull out the path requested (WEBrick keeps the host and port and protocol in REQUEST_URI) - # PATH_INFO is failover if REQUEST_URI is blank (like what Rack::MockRequest does) - uri = URI.parse(env['REQUEST_URI'] || env['PATH_INFO']).path - - # prepare request - path = (uri ? uri.split('?').first : '').sub(/\/+/, '/') - path = path[0..-2] if (path[-1] == ?/) && path.size > 1 - req = Struct.new(:path, :method).new(path, env['REQUEST_METHOD'].downcase.to_sym) - - # perform match - route = self.match(req, {}) - - # make sure a route is returned even if no match is found - if route[0].nil? - #return default route - env['halcyon.logger'].debug "No route found. Using default." if env['halcyon.logger'].is_a? Logger - @@default_route - else - # params (including action and module if set) for the matching route - route[1] - end - end - - end - end -end diff --git a/spec/halcyon/error_spec.rb b/spec/halcyon/error_spec.rb deleted file mode 100644 index 919a04d..0000000 --- a/spec/halcyon/error_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -describe "Halcyon::Server Errors" do - - before do - @app = Specr.new :port => 4000 - end - - it "should provide shorthand methods for errors which should throw an appropriate exception" do - begin - @app.not_found - rescue Halcyon::Exceptions::Base => e - e.status.should == 404 - e.error.should == 'Not Found' - end - - begin - @app.not_found('Missing') - rescue Halcyon::Exceptions::Base => e - e.status.should == 404 - e.error.should == 'Missing' - end - end - - it "supports numerous standard HTTP request error exceptions with lookup by status code" do - begin - Halcyon::Server::Base::Exceptions::NotFound.new - rescue Halcyon::Exceptions::Base => e - e.status.should == 404 - e.error.should == 'Not Found' - end - - Halcyon::Exceptions::HTTP_ERROR_CODES.each do |code, error| - begin - Halcyon::Server::Base::Exceptions.const_get(error.gsub(/( |\-)/,'')).new - rescue Halcyon::Exceptions::Base => e - e.status.should == code - e.error.should == error - end - begin - Halcyon::Server::Base::Exceptions.lookup(code).new - rescue Halcyon::Exceptions::Base => e - e.status.should == code - e.error.should == error - end - end - end - - it "should have a short inheritence chain to make catching generically simple" do - begin - Halcyon::Server::Base::Exceptions::NotFound.new - rescue Halcon::Exceptions::Base => e - e.class.to_s.should == 'NotFound' - end - end - -end diff --git a/spec/halcyon/router_spec.rb b/spec/halcyon/router_spec.rb deleted file mode 100644 index 0a0975c..0000000 --- a/spec/halcyon/router_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -describe "Halcyon::Server::Router" do - - before do - @app = Specr.new :port => 4000 - end - - it "should prepares routes correctly when written correctly" do - # routes have been defined for Specr - Halcyon::Server::Router.routes.should.not == [] - Halcyon::Server::Router.routes.length.should > 0 - end - - it "should match URIs to the correct route" do - Halcyon::Server::Router.route(Rack::MockRequest.env_for('/'))[:action].should == 'index' - end - - it "should use the default route if no matching route is found" do - Halcyon::Server::Router.route(Rack::MockRequest.env_for('/erroneous/path'))[:action].should == 'not_found' - Halcyon::Server::Router.route(Rack::MockRequest.env_for("/random/#{rand}"))[:action].should == 'not_found' - end - - it "should map params in routes to parameters" do - response = Halcyon::Server::Router.route(Rack::MockRequest.env_for('/hello/Matt')) - response[:action].should == 'greeter' - response[:name].should == 'Matt' - end - - it "should supply arbitrary routing param values included as a param even if not in the URI" do - Halcyon::Server::Router.route(Rack::MockRequest.env_for('/'))[:arbitrary].should == 'random' - end - -end diff --git a/spec/halcyon/server_spec.rb b/spec/halcyon/server_spec.rb deleted file mode 100644 index 1012b3a..0000000 --- a/spec/halcyon/server_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -describe "Halcyon::Server" do - - before do - @app = Specr.new :port => 4000 - end - - it "should dispatch methods according to their respective routes" do - Rack::MockRequest.new(@app).get("/hello/Matt") - last_line = File.new(@app.instance_variable_get("@config")[:log_file]).readlines.last - last_line.should =~ /INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specr#test :: \[200\] .* => greeter \(.+\)/ - end - - it "should provide various shorthand methods for simple responses but take custom response values" do - response = {:status => 200, :body => 'OK'} - @app.ok.should == response - @app.success.should == response - @app.standard_response.should == response - - @app.ok('').should == {:status => 200, :body => ''} - @app.ok(['OK', 'Sure Thang', 'Correcto']).should == {:status => 200, :body => ['OK', 'Sure Thang', 'Correcto']} - end - - it "should handle requests and respond with JSON" do - body = JSON.parse(Rack::MockRequest.new(@app).get("/").body) - body['status'].should == 200 - body['body'].should == "Found" - end - - it "should handle requests with param values in the URL" do - body = JSON.parse(Rack::MockRequest.new(@app).get("/hello/Matt").body) - body['status'].should == 200 - body['body'].should == "Hello Matt" - end - - it "should route unmatchable requests to the default route and return JSON with appropriate status" do - body = JSON.parse(Rack::MockRequest.new(@app).get("/garbage/request/url").body) - body['status'].should == 404 - body['body'].should == "Not Found" - end - - it "should log activity" do - prev_line = File.new(@app.instance_variable_get("@config")[:log_file]).readlines.last - Rack::MockRequest.new(@app).get("/url/that/will/not/be/found/#{rand}") - last_line = File.new(@app.instance_variable_get("@config")[:log_file]).readlines.last - last_line.should =~ /INFO \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \(\d+\) Specr#test :: \[404\] .* => not_found \(.+\)/ - prev_line.should.not == last_line - end - - it "should create a PID file while running with the correct process ID" do - pid_file = @app.instance_variable_get("@config")[:pid_file] - File.exist?(pid_file).should.be.true? - File.open(pid_file){|file|file.read.should == "#{$$}\n"} - end - - it "should parse URI query params correctly" do - Rack::MockRequest.new(@app).get("/?query=value&lang=en-US") - @app.query_params.should == {:query => 'value', :lang => 'en-US'} - end - - it "should parse the URI correctly" do - Rack::MockRequest.new(@app).get("http://localhost:4000/slaughterhouse/5") - @app.uri.should == '/slaughterhouse/5' - - Rack::MockRequest.new(@app).get("/slaughterhouse/5") - @app.uri.should == '/slaughterhouse/5' - - Rack::MockRequest.new(@app).get("") - @app.uri.should == '/' - end - - it "should provide a quick way to find out what method the request was performed using" do - Rack::MockRequest.new(@app).get("/#{rand}") - @app.method.should == :get - - Rack::MockRequest.new(@app).post("/#{rand}") - @app.method.should == :post - - Rack::MockRequest.new(@app).put("/#{rand}") - @app.method.should == :put - - Rack::MockRequest.new(@app).delete("/#{rand}") - @app.method.should == :delete - end - - it "should provide convenient access to GET and POST data" do - Rack::MockRequest.new(@app).get("/#{rand}?foo=bar") - @app.get[:foo].should == 'bar' - - Rack::MockRequest.new(@app).post("/#{rand}", :input => {:foo => 'bar'}.to_params) - @app.post[:foo].should == 'bar' - end - - it "should deny all unacceptable requests" do - conf = @app.instance_variable_get("@config") - conf[:acceptable_requests] = Halcyon::Server::ACCEPTABLE_REQUESTS - - Rack::MockRequest.new(@app).get("/#{rand}") - @app.acceptable_request! rescue Halcyon::Exceptions::Base - end - - it "should record the correct environment details" do - @app.instance_eval { @config[:root].should == Dir.pwd } - end - -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index ce2eef3..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'halcyon/server' -require 'rack/mock' - -$test = true - -class Specr < Halcyon::Server::Base - - route do |r| - r.match('/hello/:name').to(:action => 'greeter') - r.match('/').to(:action => 'index', :arbitrary => 'random') - end - - def index - ok('Found') - end - - def greeter - ok("Hello #{params[:name]}") - end - -end From 1f6d6aba549f0c34fea4d0f8a58c76bf9adb121a Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Mon, 17 Mar 2008 05:36:54 -0400 Subject: [PATCH 02/27] Initial port work from the HTML files to HAML. Expanded the Rakefile to manage all of the tasks essential to generating, cleaning, and updating the website. Ignored the compiled folder to require recompile each time the website needs to be updated. --- .gitignore | 2 + Rakefile | 92 ++++++++++++++++++++++++++++++++++ source/index.haml | 89 ++++++++++++++++++++++++++++++++ source/stylesheets/styles.sass | 0 4 files changed, 183 insertions(+) create mode 100644 .gitignore create mode 100644 Rakefile create mode 100644 source/index.haml create mode 100644 source/stylesheets/styles.sass diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69b02a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +compiled/* diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b7a1a1b --- /dev/null +++ b/Rakefile @@ -0,0 +1,92 @@ +# Created by elliottcable: elliottcable.name +# Licensed as Creative Commons BY-NC-SA 3.0 (creativecommons.org/licenses/by-nc-sa/3.0) +require 'haml/engine' +require 'sass/engine' + +task :default => ['site:update'] + +namespace(:site) do + + desc 'Update the website' + task :update => ['haml:compile', 'sass:compile'] do + `rsync -avz ./compiled/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/test/ > /dev/null` + puts "* uploaded ./compiled/ to http://halcyon.rubyforge.org/test/" + end + +end + +namespace(:sass) do + + desc 'Make compiled/stylesheets/ directory if nonexistent' + task :mkdir => ['haml:mkdir'] do + unless File.exist?('./compiled/stylesheets/') + Dir.mkdir('./compiled/stylesheets/') + puts "* created ./compiled/stylesheets/" + end + end + + desc 'Clear compiled CSS files' + task :clear do + Dir['./compiled/stylesheets/*.css'].each do |file| + File.delete file + puts "* deleted #{File.basename(file)}" + end + end + + desc 'Compile SASS templates to CSS' + task :compile => [:mkdir, :clear] do + Dir['./source/stylesheets/*.sass'].each do |sass_filename| + File.open(sass_filename, 'r') do |sass_file| + css_filename = sass_filename.gsub('.sass', '.css').gsub('./source/', './compiled/') + File.open(css_filename, 'w') do |css_file| + css_file << Sass::Engine.new(sass_file.read, :style => :compressed, :filename => sass_filename.to_s, :load_paths => ['.']).to_css + puts "* compiled #{File.basename(css_file.path)}" + end + end + end + end + +end + +namespace(:haml) do + + desc 'Make compiled/ directory if nonexistent' + task :mkdir do + unless File.exist?('./compiled/') + Dir.mkdir('./compiled/') + puts "* created ./compiled/" + end + end + + desc 'Clear compiled HTML files' + task :clear do + Dir['./compiled/*.html'].each do |file| + File.delete file + puts "* deleted #{File.basename(file)}" + end + end + + desc 'Compile HAML templates to HTML' + task :compile => [:mkdir, :clear] do + Dir['./source/*.haml'].each do |haml_filename| + File.open(haml_filename, 'r') do |haml_file| + html_filename = haml_filename.gsub('.haml', '.html').gsub('./source/', './compiled/') + File.open(html_filename, 'w') do |html_file| + html_file << Haml::Engine.new(haml_file.read, :filename => haml_filename.to_s, :load_paths => ['.']).to_html + puts "* compiled #{File.basename(html_file.path)}" + end + end + end + end + +end + +desc 'Clear all compiled files' +task :clear => ['sass:clear', 'haml:clear'] do + Dir.rmdir('./compiled/stylesheets/') + puts "* deleted ./compiled/stylesheets/" + + Dir.rmdir('./compiled/') + puts "* deleted ./compiled/" +end + diff --git a/source/index.haml b/source/index.haml new file mode 100644 index 0000000..5e0ff73 --- /dev/null +++ b/source/index.haml @@ -0,0 +1,89 @@ +!!! 1.0 Strict +%html{:xmlns => 'http://www.w3.org/1999/xhtml', :"xml:lang" => 'en', :lang => 'en'} + %head + %title Halcyon + %meta{:name => 'content-type', :content => 'text/html;charset=utf-8'} + %meta{:name => 'author', :content => 'Igor Pengivrag (www.colorlightstudio.com)'} + %meta{:name => 'description', :content => 'Halcyon, Ruby JSON App Framework'} + %meta{:name => 'keywords', :content => 'ruby, json, framework, soa, http'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => '/style.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => 'stylesheets/styles.css', :media => 'screen'} + %body + #wrap + #top + %h2 + %a{:href => '/', :title => 'Home'} Halcyon + #menu + %ul + %li + %a.current{:href => '/'} home + %li + %a.current{:href => '/doc/'} RDoc + %li + %a.current{:href => 'http://ohloh.net/projects/10313?p=Halcyon'} Ohloh + %li + %a.current{:href => 'http://github.com/mtodd/halcyon'} GitHub + #content + #left + %h2 Introduction + %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. + %p Halcyon has several aims and goals, including: + %ul + %li Be fast + %li Be small + %li Talk small + %li Be flexible + %li Be easy to implement + + %h2 Functionality & Performance + %p With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. + %code Coming soon... + %p You can then run it with + %code $ thin start -r runner.ru -p 4647 + %p That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. + + %h2 Metrics + %p Ohloh's pretty cool and we use it to track the development metrics of Halcyon. Check out some of the more interesting details on our project page. You can find the link at the top of the page. + %p For your viewing pleasure, here are some of the Ohloh project factoids: + %script{:type => 'text/javascript', :src => 'http://www.ohloh.net/projects/10313/widgets/project_factoids'} + + #right + .box + %h2 About + %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. + + %h2 Recent Entries + #twitter_div + %ul#twitter_update_list + + %h2 Installation + %p Installation is easy, all you need is RubyGems and you're set. Just run the following: + %code $ sudo gem install halcyon + %p If you're interested in the most recent version, check out our GitHub page (link above). You can install the latest development version with these steps: + %code + $ git clone git://github.com/mtodd/halcyon.git + $ cd halcyon && rake install + %p This will get the latest version of Halcyon and run the install task. + %p + %strong Note: + Due to limitations, only json_pure is a dependency since it is supported on all platforms. However, for faster performance, make sure you run this command which will install the faster C extension of JSON: + %code $ sudo gem install json + + %h2 Community + %p Welcome to the infantile community, I really hope you do choose to stay with us for a while and help us get our feet on the ground. + %p There are several ways to participate: on IRC, you can find us at #halcyon on irc.freenode.net. Join us on our mailing list hosted by GOogle Groups. Regardless, you can always get a hold of Matt Todd via his email address. + + #clear + + #footer + %p Halcyon © 2007-2008 Matt Todd. Design by Color Light Studio. + + %script{:type => 'text/javascript'} + var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); + document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); + %script{:type => 'text/javascript'} + var pageTracker = _gat._getTracker("UA-101852-3"); + pageTracker._initData(); + pageTracker._trackPageview(); + %script{:type => 'text/javascript', :src => 'http://twitter.com/javascripts/blogger.js'} + %script{:type => 'text/javascript', :src => 'http://twitter.com/javascripts/statuses/user_timeline/halcyon_dev.json?callback=twitterCallback2&count=5'} diff --git a/source/stylesheets/styles.sass b/source/stylesheets/styles.sass new file mode 100644 index 0000000..e69de29 From 9da13e4024314cca2c2c894053590d6a0ff176e7 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Mon, 17 Mar 2008 05:43:09 -0400 Subject: [PATCH 03/27] Fixed error with the Twitter javascript references. Recent Entries now loads properly. --- source/index.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/index.haml b/source/index.haml index 5e0ff73..8513165 100644 --- a/source/index.haml +++ b/source/index.haml @@ -86,4 +86,4 @@ pageTracker._initData(); pageTracker._trackPageview(); %script{:type => 'text/javascript', :src => 'http://twitter.com/javascripts/blogger.js'} - %script{:type => 'text/javascript', :src => 'http://twitter.com/javascripts/statuses/user_timeline/halcyon_dev.json?callback=twitterCallback2&count=5'} + %script{:type => 'text/javascript', :src => 'http://twitter.com/statuses/user_timeline/halcyon_dev.json?callback=twitterCallback2&count=5'} From ae902bce05065199b8579bf3e64e38b1f098ade2 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Wed, 19 Mar 2008 22:51:34 -0400 Subject: [PATCH 04/27] Converted stylesheet to Sass. Added content and moved some portions of text over to Textile to allow for great formatting control. Pointed the Rakefile to update the primary website. --- Rakefile | 7 +- source/index.haml | 54 ++++++--- source/stylesheets/styles.sass | 207 +++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 17 deletions(-) diff --git a/Rakefile b/Rakefile index b7a1a1b..ef195fc 100644 --- a/Rakefile +++ b/Rakefile @@ -9,8 +9,8 @@ namespace(:site) do desc 'Update the website' task :update => ['haml:compile', 'sass:compile'] do - `rsync -avz ./compiled/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/test/ > /dev/null` - puts "* uploaded ./compiled/ to http://halcyon.rubyforge.org/test/" + `rsync -avz ./compiled/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/ > /dev/null` + puts "* uploaded ./compiled/ to http://halcyon.rubyforge.org/" end end @@ -35,11 +35,12 @@ namespace(:sass) do desc 'Compile SASS templates to CSS' task :compile => [:mkdir, :clear] do + style = (ENV['SASS_STYLE'] || 'compressed').to_sym Dir['./source/stylesheets/*.sass'].each do |sass_filename| File.open(sass_filename, 'r') do |sass_file| css_filename = sass_filename.gsub('.sass', '.css').gsub('./source/', './compiled/') File.open(css_filename, 'w') do |css_file| - css_file << Sass::Engine.new(sass_file.read, :style => :compressed, :filename => sass_filename.to_s, :load_paths => ['.']).to_css + css_file << Sass::Engine.new(sass_file.read, :style => style, :filename => sass_filename.to_s, :load_paths => ['.']).to_css puts "* compiled #{File.basename(css_file.path)}" end end diff --git a/source/index.haml b/source/index.haml index 8513165..59006d4 100644 --- a/source/index.haml +++ b/source/index.haml @@ -6,7 +6,6 @@ %meta{:name => 'author', :content => 'Igor Pengivrag (www.colorlightstudio.com)'} %meta{:name => 'description', :content => 'Halcyon, Ruby JSON App Framework'} %meta{:name => 'keywords', :content => 'ruby, json, framework, soa, http'} - %link{:rel => 'stylesheet', :type => 'text/css', :href => '/style.css', :media => 'screen'} %link{:rel => 'stylesheet', :type => 'text/css', :href => 'stylesheets/styles.css', :media => 'screen'} %body #wrap @@ -16,36 +15,59 @@ #menu %ul %li - %a.current{:href => '/'} home + %a.current{:href => '/'} Home %li - %a.current{:href => '/doc/'} RDoc + %a{:href => '/doc/'} RDoc %li - %a.current{:href => 'http://ohloh.net/projects/10313?p=Halcyon'} Ohloh + %a{:href => 'http://ohloh.net/projects/10313?p=Halcyon'} Ohloh %li - %a.current{:href => 'http://github.com/mtodd/halcyon'} GitHub + %a{:href => 'http://github.com/mtodd/halcyon'} GitHub #content #left %h2 Introduction %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. %p Halcyon has several aims and goals, including: - %ul - %li Be fast - %li Be small - %li Talk small - %li Be flexible - %li Be easy to implement + :textile + * *Be fast* -- easy with "Rack":http://rack.rubyforge.org/ and "Mongrel":http://mongrel.rubyforge.org/ or "Thin":http://code.macournoyer.com/thin + * *Be small* -- also not a problem with Rack and Mongrel + * *Be cross-platform* -- communications are flexible with JSON transport layer + * *Be flexible* -- since it uses HTTP, it's very simple to be flexible + * *Be easy to implement* -- also easy since we're developing in "Ruby":http://ruby-lang.org/ here %h2 Functionality & Performance %p With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. - %code Coming soon... + %code + %pre + :preserve + class Queue < Application + @queue = [] + def enqueue + @queue << params[:body] + ok + end + def dequeue + ok @queue.shift + end + def list + ok @queue + end + end %p You can then run it with %code $ thin start -r runner.ru -p 4647 %p That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. + :textile + Read the "Getting Started Tutorial":#. + + %h2 Supported Platforms + %p Halcyon is primarily written in Ruby, but Halcyon also supports multiple platforms due to the fact that it communicates via HTTP and packages its messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, with more clients planned. %h2 Metrics %p Ohloh's pretty cool and we use it to track the development metrics of Halcyon. Check out some of the more interesting details on our project page. You can find the link at the top of the page. %p For your viewing pleasure, here are some of the Ohloh project factoids: %script{:type => 'text/javascript', :src => 'http://www.ohloh.net/projects/10313/widgets/project_factoids'} + %p.small + %strong Note: + It says that Halcyon is mostly written in Java because we include the client libraries in the code base, each of which can have their own extensive dependencies, including the Java one. Since Ruby's syntax is much leaner, Java's line count overtakes the Ruby line count even though the Java code is only for the client library. #right .box @@ -70,13 +92,17 @@ %code $ sudo gem install json %h2 Community - %p Welcome to the infantile community, I really hope you do choose to stay with us for a while and help us get our feet on the ground. - %p There are several ways to participate: on IRC, you can find us at #halcyon on irc.freenode.net. Join us on our mailing list hosted by GOogle Groups. Regardless, you can always get a hold of Matt Todd via his email address. + :textile + Welcome to the infantile community, I really hope you do choose to stay with us for a while and help us get our feet on the ground. + + There are several ways to participate: on IRC, you can find us at #halcyon on irc.freenode.net. Join us on "our mailing list":http://groups.google.com/group/halcyon-dev hosted by GOogle Groups. Regardless, you can always get a hold of Matt Todd via his "email address":mailto:chiology@gmail.com. #clear #footer %p Halcyon © 2007-2008 Matt Todd. Design by Color Light Studio. + %p.small + == Last updated: #{Time.now.strftime('%d %b %Y')} %script{:type => 'text/javascript'} var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); diff --git a/source/stylesheets/styles.sass b/source/stylesheets/styles.sass index e69de29..6bac0f0 100644 --- a/source/stylesheets/styles.sass +++ b/source/stylesheets/styles.sass @@ -0,0 +1,207 @@ +/* Created by Igor Penjivrag (www.colorlightstudio.com) - 12.11.2006 + Modified by Matt Todd (maraby.org) for personal use. + Converted to Sass by Matt Todd (maraby.org). + +!highlight = #FF5938 + +body + :margin 0px + :background url(../img/top_bg.gif) + :background-repeat repeat-x + :font-family Verdana, Arial, sans-serif + :font-size 0.85em + +p + :line-height 17px + :margin 11px 0 10px 0 + :padding 0px + + .small + :text-size 80% + :color #BBB + +h2 + :color #73353A + :margin 0px + :padding 0px + :font-size 15px + +ul + :font-size 10px + :margin 0 + :padding 0 + :list-style-image url(../img/bullet.gif) + +a + :color #8B0000 + &:hover + :text-decoration none + +blockquote + :background #F7FDE3 + :color #606060 + :padding 10px + +code + :background #F7FDE3 + :color #606060 + :font-size 125% + + +/* Main Container + +#wrap + :margin-left auto + :margin-right auto + :width 730px + + +/* Top + +#top + :width 100% + :height 88px + :color #fff + :background #000 url(../img/top_bg.gif) + :overflow hidden + + h2 + :color #fff + :letter-spacing 3px + :font-size 2.4em + :font-weight normal + :position relative + :margin 0px + :top 33px + :display block + :float left + :background url(../img/halcyon_is.png) no-repeat + :padding-left 40px + + a + :color white + :text-decoration none + &:hover + :color = !highlight + + +/* Main Menu + +#menu + :display block + :float right + + ul + :margin 0 + :list-style none + + li + :display block + :float left + :white-space nowrap + + a + :display block + :padding 55px 20px 12px 20px + :text-decoration none + :color #fff + &:hover + :background = !highlight + + .current + :letter-spacing 1px + :color gray + &:hover + :color #fff + +* html #menu a + :width 1% + + +/* Content Container + +#content + :width 100% + :margin-top 30px + + h2 + :margin 0 + :padding 10px 0 10px 0 + + +/* Content + +#left + :width 350px + :float left + :display block + :margin-left 20px + :display inline + + ul + :padding 15px 0 15px 35px + :margin 0 + + li + :margin-bottom 5px + + +/* Sidebar + +#right + :width 315px + :float right + :display block + :margin-top 10px + + .box + :width 280px + :background #F6F9FB + :border 1px solid #E1E1E1 + :padding 10px 10px 15px 10px + :float right + + h2 + :font-size 1.1em + :margin 0px 0 0px 0 + :padding 0px 0 5px 0 + + a + :margin 10px 0 10px 0 + :color #56677C + :font-size 10px + + p + :margin 5px 0 10px 0 + :line-height 15px + + ul + :padding 0 0 7px 20px + :margin 10px 0 10px 0 + + li + :margin-top 5px + + +/* Clear Div + +#clear + :display block + :clear both + :width 100% + :height 1px + :overflow hidden + + +/* Footer + +#footer + :margin 40px auto 0 auto + :text-align center + :border-top dotted 1px gray + :padding 20px 0 20px 0 + :width 70% + + p + :margin 0px + :padding 0 From 654e5a03e7b9cabb22ce5d851be6ad8c2507f222 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 25 Mar 2008 17:43:05 -0400 Subject: [PATCH 05/27] Updated the example of Queue to QueueController since Queue would technically be a syntax error since Queue exists already. --- source/index.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/index.haml b/source/index.haml index 59006d4..5758536 100644 --- a/source/index.haml +++ b/source/index.haml @@ -39,7 +39,7 @@ %code %pre :preserve - class Queue < Application + class QueueController < Application @queue = [] def enqueue @queue << params[:body] From 2b314708a5ae8f5d60b2354b7bee5b3c9cbbd3b7 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Fri, 4 Apr 2008 00:11:20 -0400 Subject: [PATCH 06/27] Moved to Webby site structure, keeping current site style. --- .gitignore | 1 + Rakefile | 97 +-- content/about.txt | 11 + content/css/blueprint/License.txt | 21 + content/css/blueprint/Readme.txt | 100 +++ content/css/blueprint/compressed/print.css | 76 ++ content/css/blueprint/compressed/screen.css | 696 ++++++++++++++++++ content/css/blueprint/lib/forms.css | 45 ++ content/css/blueprint/lib/grid.css | 193 +++++ content/css/blueprint/lib/grid.png | Bin 0 -> 206 bytes content/css/blueprint/lib/ie.css | 30 + content/css/blueprint/lib/reset.css | 39 + content/css/blueprint/lib/typography.css | 116 +++ content/css/blueprint/plugins/buttons/Readme | 31 + .../css/blueprint/plugins/buttons/buttons.css | 97 +++ .../blueprint/plugins/buttons/icons/cross.png | Bin 0 -> 655 bytes .../blueprint/plugins/buttons/icons/key.png | Bin 0 -> 455 bytes .../blueprint/plugins/buttons/icons/tick.png | Bin 0 -> 537 bytes .../css/blueprint/plugins/css-classes/Readme | 14 + .../plugins/css-classes/css-classes.css | 24 + .../css/blueprint/plugins/fancy-type/Readme | 22 + .../fancy-type/fancy-type-compressed.css | 5 + .../plugins/fancy-type/fancy-type.css | 74 ++ content/css/blueprint/print.css | 68 ++ content/css/blueprint/screen.css | 22 + content/css/coderay.css | 111 +++ content/css/site.css | 67 ++ .../stylesheets => content/css}/styles.sass | 14 +- content/img/bullet.gif | Bin 0 -> 82 bytes content/img/h_lcyon_large.png | Bin 0 -> 13520 bytes content/img/h_lcyon_small.png | Bin 0 -> 4358 bytes content/img/h_lcyon_small_i.png | Bin 0 -> 4470 bytes content/img/h_lcyon_small_i_x150.png | Bin 0 -> 4968 bytes content/img/halcyon.png | Bin 0 -> 6801 bytes content/img/halcyon_i.png | Bin 0 -> 6704 bytes content/img/halcyon_is.png | Bin 0 -> 3576 bytes content/img/halcyon_r.png | Bin 0 -> 7230 bytes content/img/halcyon_s.png | Bin 0 -> 3520 bytes content/img/halcyon_title.png | Bin 0 -> 64063 bytes content/img/logo.jpg | Bin 0 -> 737 bytes content/img/top_bg.gif | Bin 0 -> 113 bytes content/index.txt | 62 ++ source/index.haml => layouts/default.rhtml | 54 +- lib/breadcrumbs.rb | 28 + tasks/create.rake | 4 + tasks/deploy.rake | 22 + tasks/growl.rake | 12 + tasks/heel.rake | 28 + tasks/setup.rb | 14 + tasks/validate.rake | 19 + templates/_partial.erb | 10 + templates/atom_feed.erb | 34 + templates/page.erb | 18 + 53 files changed, 2145 insertions(+), 134 deletions(-) create mode 100644 content/about.txt create mode 100644 content/css/blueprint/License.txt create mode 100644 content/css/blueprint/Readme.txt create mode 100644 content/css/blueprint/compressed/print.css create mode 100644 content/css/blueprint/compressed/screen.css create mode 100644 content/css/blueprint/lib/forms.css create mode 100644 content/css/blueprint/lib/grid.css create mode 100644 content/css/blueprint/lib/grid.png create mode 100644 content/css/blueprint/lib/ie.css create mode 100644 content/css/blueprint/lib/reset.css create mode 100644 content/css/blueprint/lib/typography.css create mode 100644 content/css/blueprint/plugins/buttons/Readme create mode 100644 content/css/blueprint/plugins/buttons/buttons.css create mode 100644 content/css/blueprint/plugins/buttons/icons/cross.png create mode 100644 content/css/blueprint/plugins/buttons/icons/key.png create mode 100644 content/css/blueprint/plugins/buttons/icons/tick.png create mode 100644 content/css/blueprint/plugins/css-classes/Readme create mode 100644 content/css/blueprint/plugins/css-classes/css-classes.css create mode 100644 content/css/blueprint/plugins/fancy-type/Readme create mode 100644 content/css/blueprint/plugins/fancy-type/fancy-type-compressed.css create mode 100644 content/css/blueprint/plugins/fancy-type/fancy-type.css create mode 100644 content/css/blueprint/print.css create mode 100644 content/css/blueprint/screen.css create mode 100644 content/css/coderay.css create mode 100644 content/css/site.css rename {source/stylesheets => content/css}/styles.sass (92%) create mode 100644 content/img/bullet.gif create mode 100644 content/img/h_lcyon_large.png create mode 100644 content/img/h_lcyon_small.png create mode 100644 content/img/h_lcyon_small_i.png create mode 100644 content/img/h_lcyon_small_i_x150.png create mode 100644 content/img/halcyon.png create mode 100644 content/img/halcyon_i.png create mode 100644 content/img/halcyon_is.png create mode 100644 content/img/halcyon_r.png create mode 100644 content/img/halcyon_s.png create mode 100644 content/img/halcyon_title.png create mode 100644 content/img/logo.jpg create mode 100644 content/img/top_bg.gif create mode 100644 content/index.txt rename source/index.haml => layouts/default.rhtml (53%) create mode 100644 lib/breadcrumbs.rb create mode 100644 tasks/create.rake create mode 100644 tasks/deploy.rake create mode 100644 tasks/growl.rake create mode 100644 tasks/heel.rake create mode 100644 tasks/setup.rb create mode 100644 tasks/validate.rake create mode 100644 templates/_partial.erb create mode 100644 templates/atom_feed.erb create mode 100644 templates/page.erb diff --git a/.gitignore b/.gitignore index 69b02a7..1db2b94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ compiled/* +output/* diff --git a/Rakefile b/Rakefile index ef195fc..9319591 100644 --- a/Rakefile +++ b/Rakefile @@ -1,93 +1,20 @@ -# Created by elliottcable: elliottcable.name -# Licensed as Creative Commons BY-NC-SA 3.0 (creativecommons.org/licenses/by-nc-sa/3.0) -require 'haml/engine' -require 'sass/engine' +# $Id$ -task :default => ['site:update'] +load 'tasks/setup.rb' -namespace(:site) do - - desc 'Update the website' - task :update => ['haml:compile', 'sass:compile'] do - `rsync -avz ./compiled/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/ > /dev/null` - puts "* uploaded ./compiled/ to http://halcyon.rubyforge.org/" - end - -end +task :default => :build -namespace(:sass) do - - desc 'Make compiled/stylesheets/ directory if nonexistent' - task :mkdir => ['haml:mkdir'] do - unless File.exist?('./compiled/stylesheets/') - Dir.mkdir('./compiled/stylesheets/') - puts "* created ./compiled/stylesheets/" - end - end - - desc 'Clear compiled CSS files' - task :clear do - Dir['./compiled/stylesheets/*.css'].each do |file| - File.delete file - puts "* deleted #{File.basename(file)}" - end - end - - desc 'Compile SASS templates to CSS' - task :compile => [:mkdir, :clear] do - style = (ENV['SASS_STYLE'] || 'compressed').to_sym - Dir['./source/stylesheets/*.sass'].each do |sass_filename| - File.open(sass_filename, 'r') do |sass_file| - css_filename = sass_filename.gsub('.sass', '.css').gsub('./source/', './compiled/') - File.open(css_filename, 'w') do |css_file| - css_file << Sass::Engine.new(sass_file.read, :style => style, :filename => sass_filename.to_s, :load_paths => ['.']).to_css - puts "* compiled #{File.basename(css_file.path)}" - end - end - end - end - -end +desc 'deploy the site to the webserver' +task :deploy => [:build, 'deploy:rsync'] -namespace(:haml) do - - desc 'Make compiled/ directory if nonexistent' - task :mkdir do - unless File.exist?('./compiled/') - Dir.mkdir('./compiled/') - puts "* created ./compiled/" - end - end - - desc 'Clear compiled HTML files' - task :clear do - Dir['./compiled/*.html'].each do |file| - File.delete file - puts "* deleted #{File.basename(file)}" - end - end +# EOF + +namespace(:site) do - desc 'Compile HAML templates to HTML' - task :compile => [:mkdir, :clear] do - Dir['./source/*.haml'].each do |haml_filename| - File.open(haml_filename, 'r') do |haml_file| - html_filename = haml_filename.gsub('.haml', '.html').gsub('./source/', './compiled/') - File.open(html_filename, 'w') do |html_file| - html_file << Haml::Engine.new(haml_file.read, :filename => haml_filename.to_s, :load_paths => ['.']).to_html - puts "* compiled #{File.basename(html_file.path)}" - end - end - end + desc 'Update the website' + task :update => [:build] do + `rsync -avz ./output/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/test/ > /dev/null` + puts "* uploaded ./output/ to http://halcyon.rubyforge.org/test/" end end - -desc 'Clear all compiled files' -task :clear => ['sass:clear', 'haml:clear'] do - Dir.rmdir('./compiled/stylesheets/') - puts "* deleted ./compiled/stylesheets/" - - Dir.rmdir('./compiled/') - puts "* deleted ./compiled/" -end - diff --git a/content/about.txt b/content/about.txt new file mode 100644 index 0000000..fcc93d1 --- /dev/null +++ b/content/about.txt @@ -0,0 +1,11 @@ +--- +title: About +created_at: 2008-04-03 02:18:56.433003 -04:00 +filter: + - erb + - textile +--- +p(title). <%= h(@page.title) %> + +Halcyon is a JSON web app framework built on Rack. + diff --git a/content/css/blueprint/License.txt b/content/css/blueprint/License.txt new file mode 100644 index 0000000..12a1b17 --- /dev/null +++ b/content/css/blueprint/License.txt @@ -0,0 +1,21 @@ +Copyright (c) 2007 Olav Bjorkoy (http://bjorkoy.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice, and every other copyright notice found in this +software, and all the attributions in every file, and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/content/css/blueprint/Readme.txt b/content/css/blueprint/Readme.txt new file mode 100644 index 0000000..dd708e5 --- /dev/null +++ b/content/css/blueprint/Readme.txt @@ -0,0 +1,100 @@ +Blueprint CSS framework 0.5 (http://bjorkoy.com/blueprint) +---------------------------------------------------------------- + +Welcome to Blueprint! This is a CSS framework designed to +cut down on your CSS development time. It gives you a solid +foundation to build your own CSS on. Here are some of the +features BP provides out-of-the-box: + +* An easily customizable grid +* Sensible default typography +* A typographic baseline +* Perfected browser CSS reset +* A stylesheet for printing +* Absolutely no bloat + + +Setup instructions +---------------------------------------------------------------- + +Here's how you set up Blueprint on your site. + +1) Upload BP to your server, and place it in whatever folder + you'd like. A good choice would be your CSS folder. + +2) Add the following lines to every section of your + site. Make sure the link path is correct (here, BP is in my CSS folder): + + + + +3) That's it! Blueprint is now ready to shine. + + +How to use Blueprint +---------------------------------------------------------------- + +Here's a quick primer on how to use BP: +http://code.google.com/p/blueprintcss/wiki/Tutorial + +Each file is also heavily commented, so you'll +learn a lot by reading through them. + + +Files in Blueprint +---------------------------------------------------------------- + +The framework has a few files you should check out. Every file +contains lots of (hopefully) clarifying comments. + +* screen.css + This is the main file of the framework. It imports other CSS + files from the "lib" directory, and should be included on + every page. + +* print.css + This file sets some default print rules, so that printed versions + of your site looks better than they usually would. It should be + included on every page. + +* lib/grid.css + This file sets up the grid (it's true). It has a lot of classes + you apply to divs to set up any sort of column-based grid. + +* lib/typography.css + This file sets some default typography. It also has a few + methods for some really fancy stuff to do with your text. + +* lib/reset.css + This file resets CSS values that browsers tend to set for you. + +* lib/buttons.css + Provides some great CSS-only buttons. + +* lib/compressed.css + A compressed version of the core files. Use this on every live site. + See screen.css for instructions. + + +Credits +---------------------------------------------------------------- + +Many parts of BP are directly inspired by other peoples work. +You may thank them for their brilliance. However, *do not* ask +them for support or any kind of help with BP. + +* Jeff Croft [jeffcroft.com] +* Nathan Borror [playgroundblues.com] +* Christian Metts [mintchaos.com] +* Wilson Miner [wilsonminer.com] +* The Typogrify Project [code.google.com/p/typogrify] +* Eric Meyer [meyerweb.com/eric] +* Angus Turnbull [twinhelix.com] +* Khoi Vinh [subtraction.com] + +Questions, comments, suggestions or bug reports all go to +olav at bjorkoy dot com. Thanks for your interest! + + +== By Olav Bjorkoy +== http://bjorkoy.com diff --git a/content/css/blueprint/compressed/print.css b/content/css/blueprint/compressed/print.css new file mode 100644 index 0000000..87eca64 --- /dev/null +++ b/content/css/blueprint/compressed/print.css @@ -0,0 +1,76 @@ +body { +line-height:1.5; +font-family:"Helvetica Neue", "Lucida Grande", Arial, Verdana, sans-serif; +color:#000; +background:none; +font-size:10pt; +} + +.container { +background:none; +} + +h1,h2,h3,h4,h5,h6 { +font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif; +} + +code { +font:.9em "Courier New", Monaco, Courier, monospace; +} + +img { +float:left; +margin:1.5em 1.5em 1.5em 0; +} + +a img { +border:none; +} + +p img.top { +margin-top:0; +} + +hr { +background:#ccc; +color:#ccc; +width:100%; +height:2px; +border:none; +margin:2em 0; +padding:0; +} + +blockquote { +font-style:italic; +font-size:.9em; +margin:1.5em; +padding:1em; +} + +.small { +font-size:.9em; +} + +.large { +font-size:1.1em; +} + +.quiet { +color:#999; +} + +.hide { +display:none; +} + +a:link,a:visited { +background:transparent; +font-weight:700; +text-decoration:underline; +} + +a:link:after,a:visited:after { +content:" (" attr(href) ") "; +font-size:90%; +} \ No newline at end of file diff --git a/content/css/blueprint/compressed/screen.css b/content/css/blueprint/compressed/screen.css new file mode 100644 index 0000000..b15dbd3 --- /dev/null +++ b/content/css/blueprint/compressed/screen.css @@ -0,0 +1,696 @@ +html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,code,del,dfn,em,img,q,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td { +border:0; +font-weight:inherit; +font-style:inherit; +font-size:100%; +font-family:inherit; +vertical-align:baseline; +margin:0; +padding:0; +} + +body { +line-height:1.5; +background:#fff; +font-size:75%; +color:#222; +font-family:"Helvetica Neue", "Lucida Grande", Helvetica, Arial, Verdana, sans-serif; +margin:1.5em 0; +} + +table { +border-collapse:separate; +border-spacing:0; +margin-bottom:1.4em; +} + +caption,th,td { +text-align:left; +font-weight:400; +} + +blockquote:before,blockquote:after,q:before,q:after { +content:""; +} + +blockquote,q { +quotes:; +} + +a img { +border:none; +} + +h1,h2,h3,h4,h5,h6 { +color:#111; +font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; +font-weight:400; +} + +h1 { +font-size:3em; +line-height:1; +margin-bottom:.5em; +} + +h2 { +font-size:2em; +margin-bottom:.75em; +} + +h3 { +font-size:1.5em; +line-height:1; +margin-bottom:1em; +} + +h4 { +font-size:1.2em; +line-height:1.25; +margin-bottom:1.25em; +} + +h5 { +font-size:1em; +font-weight:700; +margin-bottom:1.5em; +} + +h6 { +font-size:1em; +font-weight:700; +} + +p.last { +margin-bottom:0; +} + +p img { +float:left; +margin:1.5em 1.5em 1.5em 0; +padding:0; +} + +p img.top { +margin-top:0; +} + +ul,ol { +margin:0 1.5em 1.5em; +} + +ul { +list-style-type:circle; +} + +ol { +list-style-type:decimal; +} + +dd { +margin-left:1.5em; +} + +abbr,acronym { +border-bottom:1px dotted #666; +} + +address { +margin-top:1.5em; +font-style:italic; +} + +a:focus,a:hover { +color:#000; +} + +a { +color:#009; +text-decoration:underline; +} + +blockquote { +color:#666; +font-style:italic; +margin:1.5em; +} + +em,dfn { +font-style:italic; +background:#ffc; +} + +pre,code { +white-space:pre; +margin:1.5em 0; +} + +pre,code,tt { +font:1em 'andale mono', monotype.com, 'lucida console', monospace; +line-height:1.5; +} + +tt { +display:block; +line-height:1.5; +margin:1.5em 0; +} + +th { +border-bottom:2px solid #ccc; +font-weight:700; +} + +td { +border-bottom:1px solid #ddd; +} + +th,td { +padding:4px 10px 4px 0; +} + +tfoot { +font-style:italic; +} + +caption { +background:#ffc; +} + +table .last { +padding-right:0; +} + +.small { +font-size:.8em; +margin-bottom:1.875em; +line-height:1.875em; +} + +.large { +font-size:1.2em; +line-height:2.5em; +margin-bottom:1.25em; +} + +.hide { +display:none; +} + +.highlight { +background:#ff0; +} + +.added { +color:#060; +} + +.removed { +color:#900; +} + +.top { +margin-top:0; +padding-top:0; +} + +.bottom { +margin-bottom:0; +padding-bottom:0; +} + +.container { +width:950px; +margin:0 auto; +} + +.column { +float:left; +margin-right:10px; +} + +.last { +margin-right:0; +} + +.span-1 { +width:30px; +} + +.span-2 { +width:70px; +} + +.span-3 { +width:110px; +} + +.span-4 { +width:150px; +} + +.span-5 { +width:190px; +} + +.span-6 { +width:230px; +} + +.span-7 { +width:270px; +} + +.span-8 { +width:310px; +} + +.span-9 { +width:350px; +} + +.span-10 { +width:390px; +} + +.span-11 { +width:430px; +} + +.span-12 { +width:470px; +} + +.span-13 { +width:510px; +} + +.span-14 { +width:550px; +} + +.span-15 { +width:590px; +} + +.span-16 { +width:630px; +} + +.span-17 { +width:670px; +} + +.span-18 { +width:710px; +} + +.span-19 { +width:750px; +} + +.span-20 { +width:790px; +} + +.span-21 { +width:830px; +} + +.span-22 { +width:870px; +} + +.span-23 { +width:910px; +} + +.span-24 { +width:950px; +margin:0; +} + +.append-1 { +padding-right:40px; +} + +.append-2 { +padding-right:80px; +} + +.append-3 { +padding-right:120px; +} + +.append-4 { +padding-right:160px; +} + +.append-5 { +padding-right:200px; +} + +.append-6 { +padding-right:240px; +} + +.append-7 { +padding-right:280px; +} + +.append-8 { +padding-right:320px; +} + +.append-9 { +padding-right:360px; +} + +.append-10 { +padding-right:400px; +} + +.append-11 { +padding-right:440px; +} + +.append-12 { +padding-right:480px; +} + +.append-13 { +padding-right:520px; +} + +.append-14 { +padding-right:560px; +} + +.append-15 { +padding-right:600px; +} + +.append-16 { +padding-right:640px; +} + +.append-17 { +padding-right:680px; +} + +.append-18 { +padding-right:720px; +} + +.append-19 { +padding-right:760px; +} + +.append-20 { +padding-right:800px; +} + +.append-21 { +padding-right:840px; +} + +.append-22 { +padding-right:880px; +} + +.append-23 { +padding-right:920px; +} + +.prepend-1 { +padding-left:40px; +} + +.prepend-2 { +padding-left:80px; +} + +.prepend-3 { +padding-left:120px; +} + +.prepend-4 { +padding-left:160px; +} + +.prepend-5 { +padding-left:200px; +} + +.prepend-6 { +padding-left:240px; +} + +.prepend-7 { +padding-left:280px; +} + +.prepend-8 { +padding-left:320px; +} + +.prepend-9 { +padding-left:360px; +} + +.prepend-10 { +padding-left:400px; +} + +.prepend-11 { +padding-left:440px; +} + +.prepend-12 { +padding-left:480px; +} + +.prepend-13 { +padding-left:520px; +} + +.prepend-14 { +padding-left:560px; +} + +.prepend-15 { +padding-left:600px; +} + +.prepend-16 { +padding-left:640px; +} + +.prepend-17 { +padding-left:680px; +} + +.prepend-18 { +padding-left:720px; +} + +.prepend-19 { +padding-left:760px; +} + +.prepend-20 { +padding-left:800px; +} + +.prepend-21 { +padding-left:840px; +} + +.prepend-22 { +padding-left:880px; +} + +.prepend-23 { +padding-left:920px; +} + +.border { +padding-right:4px; +margin-right:5px; +border-right:1px solid #eee; +} + +.colborder { +padding-right:24px; +margin-right:25px; +border-right:1px solid #eee; +} + +.pull-1 { +margin-left:-40px; +} + +.pull-2 { +margin-left:-80px; +} + +.pull-3 { +margin-left:-120px; +} + +.pull-4 { +margin-left:-160px; +} + +.push-0 { +margin:0 0 0 18px; +} + +.push-1 { +margin:0 -40px 0 18px; +} + +.push-2 { +margin:0 -80px 0 18px; +} + +.push-3 { +margin:0 -120px 0 18px; +} + +.push-4 { +margin:0 -160px 0 18px; +} + +.push-0,.push-1,.push-2,.push-3,.push-4 { +float:right; +} + +.box { +margin-bottom:1.5em; +background:#eee; +padding:1.5em; +} + +hr { +background:#ddd; +color:#ddd; +clear:both; +float:none; +width:100%; +height:.1em; +border:none; +margin:0 0 1.4em; +} + +hr.space { +background:#fff; +color:#fff; +} + +.clear { +display:block; +} + +.clear:after,.container:after { +content:"."; +display:block; +height:0; +clear:both; +visibility:hidden; +} + +* html .clear { +height:1%; +} + +fieldset { +border:1px solid #ccc; +margin:0 0 1.5em; +padding:1.4em; +} + +legend { +font-weight:700; +font-size:1.2em; +} + +input.text,input.title { +width:300px; +border:1px solid #bbb; +background:#f6f6f6; +margin:.5em .5em .5em 0; +padding:5px; +} + +input.title { +font-size:1.5em; +} + +textarea { +width:400px; +height:250px; +border:1px solid #bbb; +background:#eee; +margin:.5em .5em .5em 0; +padding:5px; +} + +select { +border:1px solid #ccc; +background:#f6f6f6; +width:200px; +} + +.error,.notice,.success { +margin-bottom:1em; +border:2px solid #ddd; +padding:.8em; +} + +.error { +background:#FBE3E4; +color:#D12F19; +border-color:#FBC2C4; +} + +.notice { +background:#FFF6BF; +color:#817134; +border-color:#FFD324; +} + +.success { +background:#E6EFC2; +color:#529214; +border-color:#C6D880; +} + +.error a { +color:#D12F19; +} + +.notice a { +color:#817134; +} + +.success a { +color:#529214; +} + +p,img,dl { +margin:0 0 1.5em; +} + +dl dt,strong,dfn,label { +font-weight:700; +} + +del,.quiet { +color:#666; +} + +input.text:focus,input.title:focus,textarea:focus,select:focus { +background:#fff; +border:1px solid #999; +} \ No newline at end of file diff --git a/content/css/blueprint/lib/forms.css b/content/css/blueprint/lib/forms.css new file mode 100644 index 0000000..cf0bbca --- /dev/null +++ b/content/css/blueprint/lib/forms.css @@ -0,0 +1,45 @@ +/* -------------------------------------------------------------- + + forms.css + * Sets up some default styling for forms + * Gives you classes to enhance your forms + + Usage: + * For text fields, use class .title or .text + +-------------------------------------------------------------- */ + +label { font-weight: bold; } + + +/* Fieldsets */ +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } + +/* Text fields */ +input.text, input.title { width: 300px; margin:0.5em 0.5em 0.5em 0; } +input.text, input.title { border:1px solid #bbb; background:#f6f6f6; padding:5px; } +input.text:focus, +input.title:focus { border:1px solid #999; background:#fff; } +input.title { font-size:1.5em; } + +/* Textareas */ +textarea { width: 400px; height: 250px; margin:0.5em 0.5em 0.5em 0; } +textarea { border:1px solid #bbb; background:#eee; padding:5px; } +textarea:focus { border:1px solid #999; background:#fff; } + +/* Select fields */ +select { border:1px solid #ccc; background:#f6f6f6; width:200px; } +select:focus { border:1px solid #999; background:#fff; } + + +/* Success, error & notice boxes for messages and errors. */ +.error, +.notice, +.success { padding: .8em; margin-bottom: 1em; border: 2px solid #ddd; } +.error { background: #FBE3E4; color: #D12F19; border-color: #FBC2C4; } +.notice { background: #FFF6BF; color: #817134; border-color: #FFD324; } +.success { background: #E6EFC2; color: #529214; border-color: #C6D880; } +.error a { color: #D12F19; } +.notice a { color: #817134; } +.success a { color: #529214; } diff --git a/content/css/blueprint/lib/grid.css b/content/css/blueprint/lib/grid.css new file mode 100644 index 0000000..3986311 --- /dev/null +++ b/content/css/blueprint/lib/grid.css @@ -0,0 +1,193 @@ +/* -------------------------------------------------------------- + + grid.css + * Sets up an easy-to-use grid of 24 columns. + + Based on work by: + * Nathan Borror [playgroundblues.com] + * Jeff Croft [jeffcroft.com] + * Christian Metts [mintchaos.com] + * Khoi Vinh [subtraction.com] + + By default, the grid is 950px wide, with 24 columns + spanning 30px, and a 10px margin between columns. + + If you need fewer or more columns, use this + formula to find the new total width: + Total width = (columns * 40) - 10 + + Read more about using a grid here: + * subtraction.com/archives/2007/0318_oh_yeeaahh.php + +-------------------------------------------------------------- */ + +/* A container should group all your columns. */ +.container { + width: 950px; + margin: 0 auto; +} + + +/* Columns +-------------------------------------------------------------- */ + +/* Use this class together with the .span-x classes + to create any composition of columns in a layout. */ + +.column { + float: left; + margin-right: 10px; +} + + +/* The last column in a row needs this class. */ +.last { margin-right: 0; } + +/* Use these classes to set the width of a column. */ +.span-1 { width: 30px; } +.span-2 { width: 70px; } +.span-3 { width: 110px; } +.span-4 { width: 150px; } +.span-5 { width: 190px; } +.span-6 { width: 230px; } +.span-7 { width: 270px; } +.span-8 { width: 310px; } +.span-9 { width: 350px; } +.span-10 { width: 390px; } +.span-11 { width: 430px; } +.span-12 { width: 470px; } +.span-13 { width: 510px; } +.span-14 { width: 550px; } +.span-15 { width: 590px; } +.span-16 { width: 630px; } +.span-17 { width: 670px; } +.span-18 { width: 710px; } +.span-19 { width: 750px; } +.span-20 { width: 790px; } +.span-21 { width: 830px; } +.span-22 { width: 870px; } +.span-23 { width: 910px; } +.span-24 { width: 950px; margin: 0; } + +/* Add these to a column to append empty cols. */ +.append-1 { padding-right: 40px; } +.append-2 { padding-right: 80px; } +.append-3 { padding-right: 120px; } +.append-4 { padding-right: 160px; } +.append-5 { padding-right: 200px; } +.append-6 { padding-right: 240px; } +.append-7 { padding-right: 280px; } +.append-8 { padding-right: 320px; } +.append-9 { padding-right: 360px; } +.append-10 { padding-right: 400px; } +.append-11 { padding-right: 440px; } +.append-12 { padding-right: 480px; } +.append-13 { padding-right: 520px; } +.append-14 { padding-right: 560px; } +.append-15 { padding-right: 600px; } +.append-16 { padding-right: 640px; } +.append-17 { padding-right: 680px; } +.append-18 { padding-right: 720px; } +.append-19 { padding-right: 760px; } +.append-20 { padding-right: 800px; } +.append-21 { padding-right: 840px; } +.append-22 { padding-right: 880px; } +.append-23 { padding-right: 920px; } + +/* Add these to a column to prepend empty cols. */ +.prepend-1 { padding-left: 40px; } +.prepend-2 { padding-left: 80px; } +.prepend-3 { padding-left: 120px; } +.prepend-4 { padding-left: 160px; } +.prepend-5 { padding-left: 200px; } +.prepend-6 { padding-left: 240px; } +.prepend-7 { padding-left: 280px; } +.prepend-8 { padding-left: 320px; } +.prepend-9 { padding-left: 360px; } +.prepend-10 { padding-left: 400px; } +.prepend-11 { padding-left: 440px; } +.prepend-12 { padding-left: 480px; } +.prepend-13 { padding-left: 520px; } +.prepend-14 { padding-left: 560px; } +.prepend-15 { padding-left: 600px; } +.prepend-16 { padding-left: 640px; } +.prepend-17 { padding-left: 680px; } +.prepend-18 { padding-left: 720px; } +.prepend-19 { padding-left: 760px; } +.prepend-20 { padding-left: 800px; } +.prepend-21 { padding-left: 840px; } +.prepend-22 { padding-left: 880px; } +.prepend-23 { padding-left: 920px; } + + +/* Border on right hand side of a column. */ +.border { + padding-right: 4px; + margin-right: 5px; + border-right: 1px solid #eee; +} + +/* Border with more whitespace, spans one column. */ +.colborder { + padding-right: 24px; + margin-right: 25px; + border-right: 1px solid #eee; +} + + +/* Use these classes on an element to push it into the + next column, or to pull it into the previous column. */ + +.pull-1 { margin-left: -40px; } +.pull-2 { margin-left: -80px; } +.pull-3 { margin-left: -120px; } +.pull-4 { margin-left: -160px; } + +.push-0 { margin: 0 0 0 18px; } +.push-1 { margin: 0 -40px 0 18px; } +.push-2 { margin: 0 -80px 0 18px; } +.push-3 { margin: 0 -120px 0 18px; } +.push-4 { margin: 0 -160px 0 18px; } +.push-0, .push-1, .push-2, .push-3, .push-4 { float: right; } + + +/* Misc classes and elements +-------------------------------------------------------------- */ + +/* Use a .box to create a padded box inside a column. */ +.box { + padding: 1.5em; + margin-bottom: 1.5em; + background: #eee; +} + +/* Use this to create a horizontal ruler across a column. */ +hr { + background: #ddd; + color: #ddd; + clear: both; + float: none; + width: 100%; + height: .1em; + margin: 0 0 1.4em; + border: none; +} +hr.space { + background: #fff; + color: #fff; +} + +/* Clearing floats without extra markup + Based on How To Clear Floats Without Structural Markup by PiE + [http://www.positioniseverything.net/easyclearing.html] */ + +.clear { display: inline-block; } +.clear:after, .container:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +* html .clear { height: 1%; } +.clear { display: block; } diff --git a/content/css/blueprint/lib/grid.png b/content/css/blueprint/lib/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..129d4a29fbe92688aabed5638e0c4f73a7bca818 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^8bB<>!3HEX<>xE|QY^(zo*^7SP{WbZ0pxQQctjR6 zFmQbUVMeDlCNqG7G9|7NCBgY=CFO}lsSJ)O`AMk?Zka`?<@rU~#R|^B#xt(DF$2|k zc)B=-cyuP$eEj#lzKxOL5tEL~%H%~Gtu@#d^DPnSv6>KM@XEpK;0k6FVdQ&MBb@06Zo?vj6}9 literal 0 HcmV?d00001 diff --git a/content/css/blueprint/lib/ie.css b/content/css/blueprint/lib/ie.css new file mode 100644 index 0000000..aca3787 --- /dev/null +++ b/content/css/blueprint/lib/ie.css @@ -0,0 +1,30 @@ +/* -------------------------------------------------------------- + + ie.css + + Contains every hack for Internet Explorer versions prior + to IE7, so that our core files stay sweet and nimble. + +-------------------------------------------------------------- */ + +/* Make sure the layout is centered in IE5 */ +body { text-align: center; } +.container { text-align: left; } + + +/* This fixes the problem where IE6 adds an extra 3px margin to + two columns that are floated up against each other. */ + +* html .column { overflow-x: hidden; } /* IE6 fix */ + +.pull-1, .pull-2, .pull-3, .pull-4, +.push-1, .push-2, .push-3, .push-4, +ul, ol { + position: relative; /* Keeps IE6 from cutting pulled/pushed images */ +} + +/* Fixes incorrect styling of legend in IE6 fieldsets. */ +legend { margin-bottom:1.4em; } + +/* Fixes incorrect placement of numbers in ol's in IE6/7 */ +ol { margin-left:2em; } \ No newline at end of file diff --git a/content/css/blueprint/lib/reset.css b/content/css/blueprint/lib/reset.css new file mode 100644 index 0000000..49ba3be --- /dev/null +++ b/content/css/blueprint/lib/reset.css @@ -0,0 +1,39 @@ +/* -------------------------------------------------------------- + + reset.css + * Resets default browser CSS. + + Based on work by Eric Meyer: + * meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ + +-------------------------------------------------------------- */ + +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, code, +del, dfn, em, img, q, dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; +} + + +body { line-height: 1.5; background: #fff; margin:1.5em 0; } + +/* Tables still need 'cellspacing="0"' in the markup. */ +table { border-collapse: separate; border-spacing: 0; } +caption, th, td { text-align: left; font-weight:400; } + +/* Remove possible quote marks (") from ,
. */ +blockquote:before, blockquote:after, q:before, q:after { content: ""; } +blockquote, q { quotes: "" ""; } + +a img { border: none; } + diff --git a/content/css/blueprint/lib/typography.css b/content/css/blueprint/lib/typography.css new file mode 100644 index 0000000..d9c8013 --- /dev/null +++ b/content/css/blueprint/lib/typography.css @@ -0,0 +1,116 @@ +/* -------------------------------------------------------------- + + typography.css + * Sets up some sensible default typography. + + Based on work by: + * Nathan Borror [playgroundblues.com] + * Jeff Croft [jeffcroft.com] + * Christian Metts [mintchaos.com] + * Wilson Miner [wilsonminer.com] + * Richard Rutter [clagnut.com] + + Read more about using a baseline here: + * alistapart.com/articles/settingtypeontheweb + +-------------------------------------------------------------- */ + +/* This is where you set your desired font size. The line-heights + and vertical margins are automatically calculated from this. + The percentage is of 16px (0.75 * 16px = 12px). */ + +body { font-size: 75%; } + + +/* Default fonts and colors. + If you prefer serif fonts, remove the font-family + on the headings, and apply this one to the body: + font: 1em Georgia, "lucida bright", "times new roman", serif; */ + +body { + color: #222; + font-family: "Helvetica Neue", "Lucida Grande", Helvetica, Arial, Verdana, sans-serif; +} +h1,h2,h3,h4,h5,h6 { + color: #111; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + + +/* Headings +-------------------------------------------------------------- */ + +h1,h2,h3,h4,h5,h6 { font-weight: normal; } + +h1 { font-size: 3em; line-height: 1; margin-bottom: 0.5em; } +h2 { font-size: 2em; margin-bottom: 0.75em; } +h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1em; } +h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; } +h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; } +h6 { font-size: 1em; font-weight: bold; } + + +/* Text elements +-------------------------------------------------------------- */ + +p { margin: 0 0 1.5em; } +p.last { margin-bottom: 0; } +p img { float: left; margin: 1.5em 1.5em 1.5em 0; padding: 0; } +p img.top { margin-top: 0; } /* Use this if the image is at the top of the

. */ +img { margin: 0 0 1.5em; } + +ul, ol { margin:0 1.5em 1.5em 1.5em; } +ul { list-style-type: circle; } +ol { list-style-type: decimal; } +dl { margin: 0 0 1.5em 0; } +dl dt { font-weight: bold; } +dd { margin-left: 1.5em;} + +abbr, +acronym { border-bottom: 1px dotted #666; } +address { margin-top: 1.5em; font-style: italic; } +del { color:#666; } + +a:focus, +a:hover { color: #000; } +a { color: #009; text-decoration: underline; } + +blockquote { margin: 1.5em; color: #666; font-style: italic; } +strong { font-weight: bold; } +em,dfn { font-style: italic; background: #ffc; } +dfn { font-weight: bold; } +pre,code { margin: 1.5em 0; white-space: pre; } +pre,code,tt { font: 1em 'andale mono', 'monotype.com', 'lucida console', monospace; line-height: 1.5; } +tt { display: block; margin: 1.5em 0; line-height: 1.5; } + + +/* Tables +-------------------------------------------------------------- */ + +table { margin-bottom: 1.4em; } +th { border-bottom: 2px solid #ccc; font-weight: bold; } +td { border-bottom: 1px solid #ddd; } +th,td { padding: 4px 10px 4px 0; } +tfoot { font-style: italic; } +caption { background: #ffc; } + +/* Use this if you use span-x classes on th/td. */ +table .last { padding-right: 0; } + + +/* Some default classes +-------------------------------------------------------------- */ + +.small { font-size: .8em; margin-bottom: 1.875em; line-height: 1.875em; } +.large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; } +.quiet { color: #666; } + +.hide { display: none; } +.highlight { background:#ff0; } +.added { color:#060; } +.removed { color:#900; } + +.top { margin-top:0; padding-top:0; } +.bottom { margin-bottom:0; padding-bottom:0; } + + diff --git a/content/css/blueprint/plugins/buttons/Readme b/content/css/blueprint/plugins/buttons/Readme new file mode 100644 index 0000000..dd1cfe4 --- /dev/null +++ b/content/css/blueprint/plugins/buttons/Readme @@ -0,0 +1,31 @@ +Buttons +* Gives you great looking CSS buttons, for both and + + + Change Password + + + + Cancel + diff --git a/content/css/blueprint/plugins/buttons/buttons.css b/content/css/blueprint/plugins/buttons/buttons.css new file mode 100644 index 0000000..613b6d0 --- /dev/null +++ b/content/css/blueprint/plugins/buttons/buttons.css @@ -0,0 +1,97 @@ +/* -------------------------------------------------------------- + + buttons.css + * Gives you some great CSS-only buttons. + + Created by Kevin Hale [particletree.com] + * particletree.com/features/rediscovering-the-button-element + + See Readme.txt in this folder for instructions. + +-------------------------------------------------------------- */ + +a.button, button { + display:block; + float:left; + margin:0 0.583em 0.667em 0; + padding:5px 10px 5px 7px; /* Links */ + + border:1px solid #dedede; + border-top:1px solid #eee; + border-left:1px solid #eee; + + background-color:#f5f5f5; + font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif; + font-size:100%; + line-height:130%; + text-decoration:none; + font-weight:bold; + color:#565656; + cursor:pointer; +} +button { + width:auto; + overflow:visible; + padding:4px 10px 3px 7px; /* IE6 */ +} +button[type] { + padding:4px 10px 4px 7px; /* Firefox */ + line-height:17px; /* Safari */ +} +*:first-child+html button[type] { + padding:4px 10px 3px 7px; /* IE7 */ +} +button img, a.button img{ + margin:0 3px -3px 0 !important; + padding:0; + border:none; + width:16px; + height:16px; + float:none; +} + + +/* Button colors +-------------------------------------------------------------- */ + +/* Standard */ +button:hover, a.button:hover{ + background-color:#dff4ff; + border:1px solid #c2e1ef; + color:#336699; +} +a.button:active{ + background-color:#6299c5; + border:1px solid #6299c5; + color:#fff; +} + +/* Positive */ +body .positive { + color:#529214; +} +a.positive:hover, button.positive:hover { + background-color:#E6EFC2; + border:1px solid #C6D880; + color:#529214; +} +a.positive:active { + background-color:#529214; + border:1px solid #529214; + color:#fff; +} + +/* Negative */ +body .negative { + color:#d12f19; +} +a.negative:hover, button.negative:hover { + background:#fbe3e4; + border:1px solid #fbc2c4; + color:#d12f19; +} +a.negative:active { + background-color:#d12f19; + border:1px solid #d12f19; + color:#fff; +} diff --git a/content/css/blueprint/plugins/buttons/icons/cross.png b/content/css/blueprint/plugins/buttons/icons/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..1514d51a3cf1b67e1c5b9ada36f1fd474e2d214a GIT binary patch literal 655 zcmV;A0&x9_P)uEoyT++I zn$b9r%cFfhHe2K68PkBu*@^<$y+7xQ$wJ~;c5aBx$R=xq*41Wo zhwQus_VOgm0hughj}MhOvs#{>Vg09Y8WxjWUJY5YW zJ?&8eG!59Cz=|E%Ns@013KLWOLV)CObIIj_5{>{#k%TEAMs_GbdDV`x-iYsGH z#=Z{USAQA>NY(}X7=3{K8#4^nI0$7`a(T+P4hBKZ7hk58-_j0w;$<(*=f7ic$nT z*Wgd55in08>183j3?S=MAoDDTLoLSL$!_UDxXqSf-?qdd@H%8(We~hQu&uVIo$6NV z(zMY7wn6r5i617ZGZ)-J($xXssTcN*&WujcIDRIp6J4_PqOvJ}9!p6+yo8LmAGS3~ xN#Qq?aIt$6X#&>gHs{AQG2a)rMyf zFQK~pm1x3+7!nu%-M`k}``c>^00{o_1pjWJUTfl8mg=3qGEl8H@}^@w`VUx0_$uy4 z2FhRqKX}xI*?Tv1DJd8z#F#0c%*~rM30HE1@2o5m~}ZyoWhqv>ql{V z1ZGE0lgcoK^lx+eqc*rAX1Ky;Xx3U%u#zG!m-;eD1Qsn@kf3|F9qz~|95=&g3(7!X zB}JAT>RU;a%vaNOGnJ%e1=K6eAh43c(QN8RQ6~GP%O}Jju$~Ld*%`mO1pasdf + Best used on prepositions and ampersands. */ + +.alt { + color: #666; + font-family: "Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua", Georgia, serif; + font-style: italic; + font-weight: normal; +} + + +/* For great looking quote marks in titles, replace "asdf" with: + asdf” + (That is, when the title starts with a quote mark). + (You may have to change this value depending on your font size). */ + +.dquo { margin-left: -.5em; } + + +/* Reduced size type with incremental leading + (http://www.markboulton.co.uk/journal/comments/incremental_leading/) + + This could be used for side notes. For smaller type, you don't necessarily want to + follow the 1.5x vertical rhythm -- the line-height is too much. + + Using this class, it reduces your font size and line-height so that for + every four lines of normal sized type, there is five lines of the sidenote. eg: + + New type size in em's: + 10px (wanted side note size) / 12px (existing base size) = 0.8333 (new type size in ems) + + New line-height value: + 12px x 1.5 = 18px (old line-height) + 18px x 4 = 72px + 72px / 5 = 14.4px (new line height) + 14.4px / 10px = 1.44 (new line height in em's) */ + +p.incr, .incr p { + font-size: 10px; + line-height: 1.44em; + margin-bottom: 1.5em; +} + + +/* Surround uppercase words and abbreviations with this class. + Based on work by Jørgen Arnor GÃ¥rdsø Lom [http://twistedintellect.com/] */ + +.caps { + font-variant: small-caps; + letter-spacing: 1px; + text-transform: lowercase; + font-size:1.2em; + line-height:1%; + font-weight:bold; + padding:0 2px; +} diff --git a/content/css/blueprint/print.css b/content/css/blueprint/print.css new file mode 100644 index 0000000..abd6bca --- /dev/null +++ b/content/css/blueprint/print.css @@ -0,0 +1,68 @@ +/* -------------------------------------------------------------- + + Blueprint CSS Framework Print Styles + * Gives you some sensible styles for printing pages. + See Readme file in this directory for further instructions. + + Some additions you'll want to make, customized to your markup: + #header, #footer, #navigation { display:none; } + +-------------------------------------------------------------- */ + +body { + line-height: 1.5; + font-family: "Helvetica Neue", "Lucida Grande", Arial, Verdana, sans-serif; + color:#000; + background: none; + font-size: 10pt; +} +.container { + background: none; +} + +h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; } +code { font:.9em "Courier New", Monaco, Courier, monospace; } + +img { float:left; margin:1.5em 1.5em 1.5em 0; } +a img { border:none; } +p img.top { margin-top: 0; } + +hr { + background:#ccc; + color:#ccc; + width:100%; + height:2px; + margin:2em 0; + padding:0; + border:none; +} + +blockquote { + margin:1.5em; + padding:1em; + font-style:italic; + font-size:.9em; +} + +.small { font-size: .9em; } +.large { font-size: 1.1em; } +.quiet { color: #999; } +.hide { display:none; } + +a:link, a:visited { + background: transparent; + font-weight:700; + text-decoration: underline; +} + +a:link:after, a:visited:after { + content: " (" attr(href) ") "; + font-size: 90%; +} + +/* If you're having trouble printing relative links, uncomment and customize this: + (note: This is valid CSS3, but it still won't go through the W3C CSS Validator) */ + +/* a[href^="/"]:after { + content: " (http://www.yourdomain.com" attr(href) ") "; +} */ diff --git a/content/css/blueprint/screen.css b/content/css/blueprint/screen.css new file mode 100644 index 0000000..6d9f3c3 --- /dev/null +++ b/content/css/blueprint/screen.css @@ -0,0 +1,22 @@ +/* -------------------------------------------------------------- + + Blueprint CSS Framework Screen Styles + * Version: 0.6 (21.9.2007) + * Website: http://code.google.com/p/blueprintcss/ + See Readme file in this directory for further instructions. + +-------------------------------------------------------------- */ + +@import 'lib/reset.css'; +@import 'lib/typography.css'; +@import 'lib/grid.css'; +@import 'lib/forms.css'; + +/* Plugins: + Additional functionality can be found in the plugins directory. + See the readme files for each plugin. Example: + @import 'plugins/buttons/buttons.css'; */ + +/* See the grid: + Uncomment the line below to see the grid and baseline. + .container { background: url(lib/grid.png); } */ diff --git a/content/css/coderay.css b/content/css/coderay.css new file mode 100644 index 0000000..1886568 --- /dev/null +++ b/content/css/coderay.css @@ -0,0 +1,111 @@ +.CodeRay { + padding: 0.5em; + margin-bottom: 1.3em; + background-color: #eee; + border: 1px solid #aaa; + font: 1.1em Monaco, 'Courier New', 'Terminal', monospace; + color: #100; +} +.CodeRay pre { + padding: 0px; + margin: 0px; + overflow: auto; + background-color: transparent; + border: none; +} + +div.CodeRay { } + +span.CodeRay { white-space: pre; border: 0px; padding: 2px } + +table.CodeRay { border-collapse: collapse; width: 100%; padding: 2px } +table.CodeRay td { padding: 2px 4px; vertical-align: top } + +.CodeRay .line_numbers, .CodeRay .no { + background-color: #def; + color: gray; + text-align: right; +} +.CodeRay .line_numbers tt { font-weight: bold } +.CodeRay .no { padding: 0px 4px } + +ol.CodeRay { font-size: 10pt } +ol.CodeRay li { white-space: pre } + +.CodeRay .debug { color:white ! important; background:blue ! important; } + +.CodeRay .af { color:#00C } +.CodeRay .an { color:#007 } +.CodeRay .av { color:#700 } +.CodeRay .aw { color:#C00 } +.CodeRay .bi { color:#509; font-weight:bold } +.CodeRay .c { color:#666; } + +.CodeRay .ch { color:#04D } +.CodeRay .ch .k { color:#04D } +.CodeRay .ch .dl { color:#039 } + +.CodeRay .cl { color:#B06; font-weight:bold } +.CodeRay .co { color:#036; font-weight:bold } +.CodeRay .cr { color:#0A0 } +.CodeRay .cv { color:#369 } +.CodeRay .df { color:#099; font-weight:bold } +.CodeRay .di { color:#088; font-weight:bold } +.CodeRay .dl { color:black } +.CodeRay .do { color:#970 } +.CodeRay .ds { color:#D42; font-weight:bold } +.CodeRay .e { color:#666; font-weight:bold } +.CodeRay .en { color:#800; font-weight:bold } +.CodeRay .er { color:#F00; background-color:#FAA } +.CodeRay .ex { color:#F00; font-weight:bold } +.CodeRay .fl { color:#60E; font-weight:bold } +.CodeRay .fu { color:#06B; font-weight:bold } +.CodeRay .gv { color:#d70; font-weight:bold } +.CodeRay .hx { color:#058; font-weight:bold } +.CodeRay .i { color:#00D; font-weight:bold } +.CodeRay .ic { color:#B44; font-weight:bold } + +.CodeRay .il { background: #eee } +.CodeRay .il .il { background: #ddd } +.CodeRay .il .il .il { background: #ccc } +.CodeRay .il .idl { font-weight: bold; color: #888 } + +.CodeRay .in { color:#B2B; font-weight:bold } +.CodeRay .iv { color:#33B } +.CodeRay .la { color:#970; font-weight:bold } +.CodeRay .lv { color:#963 } +.CodeRay .oc { color:#40E; font-weight:bold } +.CodeRay .of { color:#000; font-weight:bold } +.CodeRay .op { } +.CodeRay .pc { color:#038; font-weight:bold } +.CodeRay .pd { color:#369; font-weight:bold } +.CodeRay .pp { color:#579 } +.CodeRay .pt { color:#339; font-weight:bold } +.CodeRay .r { color:#080; font-weight:bold } + +.CodeRay .rx { background-color:#fff0ff } +.CodeRay .rx .k { color:#808 } +.CodeRay .rx .dl { color:#404 } +.CodeRay .rx .mod { color:#C2C } +.CodeRay .rx .fu { color:#404; font-weight: bold } + +.CodeRay .s { background-color:#fff0f0 } +.CodeRay .s .s { background-color:#ffe0e0 } +.CodeRay .s .s .s { background-color:#ffd0d0 } +.CodeRay .s .k { color:#D20 } +.CodeRay .s .dl { color:#710 } + +.CodeRay .sh { background-color:#f0fff0 } +.CodeRay .sh .k { color:#2B2 } +.CodeRay .sh .dl { color:#161 } + +.CodeRay .sy { color:#A60 } +.CodeRay .sy .k { color:#A60 } +.CodeRay .sy .dl { color:#630 } + +.CodeRay .ta { color:#070 } +.CodeRay .tf { color:#070; font-weight:bold } +.CodeRay .ts { color:#D70; font-weight:bold } +.CodeRay .ty { color:#339; font-weight:bold } +.CodeRay .v { color:#036 } +.CodeRay .xt { color:#444 } diff --git a/content/css/site.css b/content/css/site.css new file mode 100644 index 0000000..fa589f2 --- /dev/null +++ b/content/css/site.css @@ -0,0 +1,67 @@ +--- +extension: css +filter: erb +layout: nil # no layout + +color: + border: "#ddd" + header: "#111" + link: "#125AA7" + link-hover: "#000" + blockquote: "#666" + box-bg: "#eee" + highlight: "#B2CCFF" + quiet: "#666" + alt: "#666" +--- + +body { + font-family: Verdana, "Bitstream Vera Sans", sans-serif; +} + +/* Headings + * --------------------------------------------------------------------- */ +h1,h2,h3,h4,h5,h6 { color: <%= @page.color['header'] %>; } + +/* Text Elements + * --------------------------------------------------------------------- */ +a { color: <%= @page.color['link'] %>; } +a:hover { color: <%= @page.color['link-hover'] %>; } +blockquote { color: <%= @page.color['blockquote'] %>; } + +pre { + background: <%= @page.color['box-bg'] %>; + border: 1px solid <%= @page.color['border'] %>; +} + +hr { + background: <%= @page.color['highlight'] %>; + color: <%= @page.color['highlight'] %>; +} + +/* Tables + * --------------------------------------------------------------------- */ +table { + border-top: 1px solid <%= @page.color['border'] %>; + border-left: 1px solid <%= @page.color['border'] %>; +} +th,td { + border-bottom: 1px solid <%= @page.color['border'] %>; + border-right: 1px solid <%= @page.color['border'] %>; +} + +/* Default Classes + * --------------------------------------------------------------------- */ +p.quiet { color: <%= @page.color['quiet'] %>; } +.alt { color: <%= @page.color['alt'] %>; } + +p.title { + color: #111; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 2em; + margin-bottom: 0.75em; +} + +#header p.title { font-size: 3em; line-height: 1; margin-bottom: 0.5em; } + +/* EOF */ diff --git a/source/stylesheets/styles.sass b/content/css/styles.sass similarity index 92% rename from source/stylesheets/styles.sass rename to content/css/styles.sass index 6bac0f0..580a13d 100644 --- a/source/stylesheets/styles.sass +++ b/content/css/styles.sass @@ -1,3 +1,9 @@ +--- +extension: css +filter: + - erb + - sass +--- /* Created by Igor Penjivrag (www.colorlightstudio.com) - 12.11.2006 Modified by Matt Todd (maraby.org) for personal use. Converted to Sass by Matt Todd (maraby.org). @@ -6,7 +12,7 @@ body :margin 0px - :background url(../img/top_bg.gif) + :background url(/img/top_bg.gif) :background-repeat repeat-x :font-family Verdana, Arial, sans-serif :font-size 0.85em @@ -30,7 +36,7 @@ ul :font-size 10px :margin 0 :padding 0 - :list-style-image url(../img/bullet.gif) + :list-style-image url(/img/bullet.gif) a :color #8B0000 @@ -62,7 +68,7 @@ code :width 100% :height 88px :color #fff - :background #000 url(../img/top_bg.gif) + :background #000 url(/img/top_bg.gif) :overflow hidden h2 @@ -75,7 +81,7 @@ code :top 33px :display block :float left - :background url(../img/halcyon_is.png) no-repeat + :background url(/img/halcyon_is.png) no-repeat :padding-left 40px a diff --git a/content/img/bullet.gif b/content/img/bullet.gif new file mode 100644 index 0000000000000000000000000000000000000000..45bb956c39ba017c77b49f8629335c7ef92d4bfb GIT binary patch literal 82 zcmZ?wbhEHbv%;Fxq c?))?GVs#N{=sd{EeMcZ;;r5VlF;xa@0NW=P^Z)<= literal 0 HcmV?d00001 diff --git a/content/img/h_lcyon_large.png b/content/img/h_lcyon_large.png new file mode 100644 index 0000000000000000000000000000000000000000..43ce1b7f24c0b665874fb606d0e91ef5e357365e GIT binary patch literal 13520 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 literal 0 HcmV?d00001 diff --git a/content/img/h_lcyon_small.png b/content/img/h_lcyon_small.png new file mode 100644 index 0000000000000000000000000000000000000000..81cf69e5d7c18e6bebce725d18966d10025d3708 GIT binary patch literal 4358 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 IM6N<$f@{}^C;$Ke literal 0 HcmV?d00001 diff --git a/content/img/h_lcyon_small_i_x150.png b/content/img/h_lcyon_small_i_x150.png new file mode 100644 index 0000000000000000000000000000000000000000..c82b0e63af3023761ad3885afd32db3ca29e2532 GIT binary patch literal 4968 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 PZ~1_ttg1{E%q;kSdK!m! literal 0 HcmV?d00001 diff --git a/content/img/halcyon_i.png b/content/img/halcyon_i.png new file mode 100644 index 0000000000000000000000000000000000000000..34196238831e79901bf1705dc49b539a59e70fd7 GIT binary patch literal 6704 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 literal 0 HcmV?d00001 diff --git a/content/img/halcyon_s.png b/content/img/halcyon_s.png new file mode 100644 index 0000000000000000000000000000000000000000..756c08382e9b856515a196d8bb3345c1caeeda0f GIT binary patch literal 3520 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

DZ{eM9 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

7>I?&goFHWi_Bf?;3I2oO#0+=Q&gEi#DBS2CK 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 Y0HGIesG&@gR{#J207*qoM6N<$g6W-hkN^Mx literal 0 HcmV?d00001 diff --git a/content/img/logo.jpg b/content/img/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..444f8f2b1fd2459471304fc00a9c10ce31f03f1f GIT binary patch literal 737 zcmex=iF;o{=v;^GnD0RsUZK7IjyJ|1CV5fNcw z8EI*08F@HhWM^mR<>8eO5Ri}(6%>_%OAyQWe}F-dgF%Wxl$lYGfk}{&S&;Gn5un2u zm>C&?u0#M9RyGbskSK~UGYbPJ9RI-K!Mgj{ zrOovWy03m^cVGY1^Y_-ye9mxe5Caxv-nHEDL8QoODw$>v30OV$0YUHTO>CLJ}Zb=tFG z?~0<3j8m?*>Sz58f45GIm%s6LljThBRSqXFERRtWFZwWH#wq_*hnEXp(Tb?wYh6(G r@XM3_1=ID9ZaZ(1kWqg(_}BiE=?kyE;+uc{XU!Xi-Ue=l`u{fp5~%OD literal 0 HcmV?d00001 diff --git a/content/img/top_bg.gif b/content/img/top_bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..fb13d3f97c784161c1b6b7b1ef3253a7a58c1032 GIT binary patch literal 113 zcmZ?wbhEHbw;`Q0x{}VL&QZnadtXh}zdS8Lg`I6{66@9`r@4q)MD9{j*=s&Tf N=wy!7{0l4$)&PZ)F^B*F literal 0 HcmV?d00001 diff --git a/content/index.txt b/content/index.txt new file mode 100644 index 0000000..a3cb227 --- /dev/null +++ b/content/index.txt @@ -0,0 +1,62 @@ +--- +title: Home +filter: + - erb + - textile +--- +h2. Introduction + +Halcyon is a JSON Web App Framework built on Rack for speed and light weight. + +Halcyon has several aims and goals, including: + +* *Be fast* -- easy with "Rack":http://rack.rubyforge.org/ and "Mongrel":http://mongrel.rubyforge.org/ or "Thin":http://code.macournoyer.com/thin +* *Be small* -- also not a problem with Rack and Mongrel +* *Be cross-platform* -- communications are flexible with JSON transport layer +* *Be flexible* -- since it uses HTTP, it's very simple to be flexible +* *Be easy to implement* -- also easy since we're developing in "Ruby":http://ruby-lang.org/ here + + +h2. Functionality & Performance + +With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class QueueController < Application + @queue = [] + def enqueue + @queue << params[:body] + ok + end + def dequeue + ok @queue.shift + end + def list + ok @queue + end +end +<% end -%> + +You can then run it with + +$ thin start -r runner.ru -p 4647 + +That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. + +Read the "Getting Started Tutorial":#. + + +h2. Supported Platforms + +Halcyon is primarily written in Ruby, but Halcyon also supports multiple platforms due to the fact that it communicates via HTTP and packages its messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, with more clients planned. + + +h2. Metrics + +Ohloh's pretty cool and we use it to track the development metrics of Halcyon. Check out some of the more interesting details on our project page. You can find the link at the top of the page. + +For your viewing pleasure, here are some of the Ohloh project factoids: + + + +Note: It says that Halcyon is mostly written in Java because we include the client libraries in the code base, each of which can have their own extensive dependencies, including the Java one. Since Ruby's syntax is much leaner, Java's line count overtakes the Ruby line count even though the Java code is only for the client library. diff --git a/source/index.haml b/layouts/default.rhtml similarity index 53% rename from source/index.haml rename to layouts/default.rhtml index 5758536..72ffb0d 100644 --- a/source/index.haml +++ b/layouts/default.rhtml @@ -1,3 +1,9 @@ +--- +extension: html +filter: + - erb + - haml +--- !!! 1.0 Strict %html{:xmlns => 'http://www.w3.org/1999/xhtml', :"xml:lang" => 'en', :lang => 'en'} %head @@ -6,7 +12,8 @@ %meta{:name => 'author', :content => 'Igor Pengivrag (www.colorlightstudio.com)'} %meta{:name => 'description', :content => 'Halcyon, Ruby JSON App Framework'} %meta{:name => 'keywords', :content => 'ruby, json, framework, soa, http'} - %link{:rel => 'stylesheet', :type => 'text/css', :href => 'stylesheets/styles.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => 'css/styles.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => 'css/coderay.css', :media => 'screen'} %body #wrap #top @@ -24,51 +31,8 @@ %a{:href => 'http://github.com/mtodd/halcyon'} GitHub #content #left - %h2 Introduction - %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. - %p Halcyon has several aims and goals, including: - :textile - * *Be fast* -- easy with "Rack":http://rack.rubyforge.org/ and "Mongrel":http://mongrel.rubyforge.org/ or "Thin":http://code.macournoyer.com/thin - * *Be small* -- also not a problem with Rack and Mongrel - * *Be cross-platform* -- communications are flexible with JSON transport layer - * *Be flexible* -- since it uses HTTP, it's very simple to be flexible - * *Be easy to implement* -- also easy since we're developing in "Ruby":http://ruby-lang.org/ here + =@content - %h2 Functionality & Performance - %p With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. - %code - %pre - :preserve - class QueueController < Application - @queue = [] - def enqueue - @queue << params[:body] - ok - end - def dequeue - ok @queue.shift - end - def list - ok @queue - end - end - %p You can then run it with - %code $ thin start -r runner.ru -p 4647 - %p That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. - :textile - Read the "Getting Started Tutorial":#. - - %h2 Supported Platforms - %p Halcyon is primarily written in Ruby, but Halcyon also supports multiple platforms due to the fact that it communicates via HTTP and packages its messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, with more clients planned. - - %h2 Metrics - %p Ohloh's pretty cool and we use it to track the development metrics of Halcyon. Check out some of the more interesting details on our project page. You can find the link at the top of the page. - %p For your viewing pleasure, here are some of the Ohloh project factoids: - %script{:type => 'text/javascript', :src => 'http://www.ohloh.net/projects/10313/widgets/project_factoids'} - %p.small - %strong Note: - It says that Halcyon is mostly written in Java because we include the client libraries in the code base, each of which can have their own extensive dependencies, including the Java one. Since Ruby's syntax is much leaner, Java's line count overtakes the Ruby line count even though the Java code is only for the client library. - #right .box %h2 About diff --git a/lib/breadcrumbs.rb b/lib/breadcrumbs.rb new file mode 100644 index 0000000..7166763 --- /dev/null +++ b/lib/breadcrumbs.rb @@ -0,0 +1,28 @@ +# breadcrumbs.rb + +module BreadcrumbsHelper + # call-seq: + # breadcrumbs( page ) => html + # + # Create breadcrumb links for the current page. This will return an HTML + #

    object. + # + def breadcrumbs( page ) + list = ["
  • #{h(page.title)}
  • "] + loop do + page = @pages.parent_of(page) + break if page.nil? + list << "
  • #{link_to_page(page)}
  • " + end + list.reverse! + + html = "
      \n" + html << list.join("\n") + html << "\n
    \n" + html + end +end # module Breadcrumbs + +Webby::Helpers.register(BreadcrumbsHelper) + +# EOF diff --git a/tasks/create.rake b/tasks/create.rake new file mode 100644 index 0000000..aeaadac --- /dev/null +++ b/tasks/create.rake @@ -0,0 +1,4 @@ + +Rake::WebbyTask.new + +# EOF diff --git a/tasks/deploy.rake b/tasks/deploy.rake new file mode 100644 index 0000000..b3997f1 --- /dev/null +++ b/tasks/deploy.rake @@ -0,0 +1,22 @@ + +require 'rake/contrib/sshpublisher' + +namespace :deploy do + + desc 'Deploy to the server using rsync' + task :rsync do + cmd = "rsync #{SITE.rsync_args.join(' ')} " + cmd << "#{SITE.output_dir}/ #{SITE.host}:#{SITE.remote_dir}" + sh cmd + end + + desc 'Deploy to the server using ssh' + task :ssh do + Rake::SshDirPublisher.new( + SITE.host, SITE.remote_dir, SITE.output_dir + ).upload + end + +end # deploy + +# EOF diff --git a/tasks/growl.rake b/tasks/growl.rake new file mode 100644 index 0000000..bc44b31 --- /dev/null +++ b/tasks/growl.rake @@ -0,0 +1,12 @@ + +desc 'Send log events to Growl (Mac OS X only)' +task :growl do + Logging::Logger['Webby'].add_appenders(Logging::Appenders::Growl.new( + "Webby", + :layout => Logging::Layouts::Pattern.new(:pattern => "%5l - Webby\000%m"), + :coalesce => true, + :separator => "\000" + )) +end + +# EOF diff --git a/tasks/heel.rake b/tasks/heel.rake new file mode 100644 index 0000000..4e991bd --- /dev/null +++ b/tasks/heel.rake @@ -0,0 +1,28 @@ + +namespace :heel do + + desc 'Start the heel server to view website (not for Windows)' + task :start do + sh "heel --root #{SITE.output_dir} --port #{SITE.heel_port} --daemonize" + end + + desc 'Stop the heel server' + task :stop do + sh "heel --kill" + end + + task :autorun do + heel_exe = File.join(Gem.bindir, 'heel') + @heel_spawner = Spawner.new(Spawner.ruby, heel_exe, '--root', SITE.output_dir, '--port', SITE.heel_port.to_s, :pause => 86_400) + @heel_spawner.start + end + + task :autobuild => :autorun do + at_exit {@heel_spawner.stop if defined? @heel_spawner and not @heel_spawner.nil?} + end + +end + +task :autobuild => 'heel:autobuild' + +# EOF diff --git a/tasks/setup.rb b/tasks/setup.rb new file mode 100644 index 0000000..f913b12 --- /dev/null +++ b/tasks/setup.rb @@ -0,0 +1,14 @@ + +begin + require 'webby' +rescue LoadError + require 'rubygems' + require 'webby' +end + +SITE = Webby.site + +# Load the other rake files in the tasks folder +Dir.glob('tasks/*.rake').sort.each {|fn| import fn} + +# EOF diff --git a/tasks/validate.rake b/tasks/validate.rake new file mode 100644 index 0000000..58214f8 --- /dev/null +++ b/tasks/validate.rake @@ -0,0 +1,19 @@ + +namespace :validate do + + desc 'Validate hyperlinks (exclude exteranl sites)' + task :internal => :build do + Webby::LinkValidator.validate(:external => false) + end + + desc 'Validate hyperlinks (include external sites)' + task :external => :build do + Webby::LinkValidator.validate(:external => true) + end + +end # validate + +desc 'Alias to validate:internal' +task :validate => 'validate:internal' + +# EOF diff --git a/templates/_partial.erb b/templates/_partial.erb new file mode 100644 index 0000000..6c515b1 --- /dev/null +++ b/templates/_partial.erb @@ -0,0 +1,10 @@ +--- +filter: erb +--- +A partial has access to the page from which it was called. The title below will be the title of the page in which this partial is rendered. + +<%%= h(@page.title) %> + +A partial does not have access to it's own meta-data. The partial meta-data is used primarily for finding partials or for use in other pages. The filter(s) specified in the meta-data will be applied to the partial text when it is rendered. + +A partial does not require meta-data at all. They can contain just text. diff --git a/templates/atom_feed.erb b/templates/atom_feed.erb new file mode 100644 index 0000000..6a10837 --- /dev/null +++ b/templates/atom_feed.erb @@ -0,0 +1,34 @@ +--- +extension: xml +layout: false +dirty: true +filter: +- erb +--- + + + + A New Atom Feed + a really swell blog + + + <%%= Time.now.xmlschema %> + + Author's Name + author@fakesite.nil + + http://fakesite.nil/ + <%% @pages.find(:limit => 10, + :in_directory => 'articles', + :recursive => true, + :sort_by => 'created_at', + :reverse => true).each do |article| %> + + <%%= h(article.title) %> + + tag:fakesite.nil,<%%= article.created_at.strftime('%Y-%m-%d') %>:<%%= article.created_at.to_i %> + <%%= article.created_at.xmlschema %> + <%%= h(article.render) %> + + <%% end %> + diff --git a/templates/page.erb b/templates/page.erb new file mode 100644 index 0000000..24d1df7 --- /dev/null +++ b/templates/page.erb @@ -0,0 +1,18 @@ +--- +title: New Page +created_at: <%= Time.now.to_y %> +filter: + - erb + - textile +--- +p(title). <%%= h(@page.title) %> + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc congue ipsum vestibulum libero. Aenean vitae justo. Nam eget tellus. Etiam convallis, est eu lobortis mattis, lectus tellus tempus felis, a ultricies erat ipsum at metus. + +h2. Litora Sociis + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi et risus. Aliquam nisl. Nulla facilisi. Cras accumsan vestibulum ante. Vestibulum sed tortor. Praesent tempus fringilla elit. Ut elit diam, sagittis in, nonummy in, gravida non, nunc. Ut orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Nam egestas, orci eu imperdiet malesuada, nisl purus fringilla odio, quis commodo est orci vitae justo. Aliquam placerat odio tincidunt nulla. Cras in libero. Aenean rutrum, magna non tristique posuere, erat odio eleifend nisl, non convallis est tortor blandit ligula. Nulla id augue. + +bq. Nullam mattis, odio ut tempus facilisis, metus nisl facilisis metus, auctor consectetuer felis ligula nec mauris. Vestibulum odio erat, fermentum at, commodo vitae, ultrices et, urna. Mauris vulputate, mi pulvinar sagittis condimentum, sem nulla aliquam velit, sed imperdiet mi purus eu magna. Nulla varius metus ut eros. Aenean aliquet magna eget orci. Class aptent taciti sociosqu ad litora. + +Vivamus euismod. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse vel nibh ut turpis dictum sagittis. Aliquam vel velit a elit auctor sollicitudin. Nam vel dui vel neque lacinia pretium. Quisque nunc erat, venenatis id, volutpat ut, scelerisque sed, diam. Mauris ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec mattis. Morbi dignissim sollicitudin libero. Nulla lorem. From ba434ee08f75cfb56aab414957be7a34722ebe24 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Fri, 4 Apr 2008 05:50:09 -0400 Subject: [PATCH 07/27] Updated website extensively with new documentation section including planned documents as well as a few fully or partially written articles. --- Rakefile | 4 +- content/css/styles.sass | 10 ++++ content/docs/clients.html | 10 ++++ content/docs/controllers.html | 10 ++++ content/docs/databases.txt | 10 ++++ content/docs/getting_started.txt | 28 ++++++++++ content/docs/index.txt | 40 +++++++++++++ content/docs/installation.txt | 66 ++++++++++++++++++++++ content/docs/introduction.html | 10 ++++ content/docs/rack.txt | 54 ++++++++++++++++++ content/docs/routes.html | 69 +++++++++++++++++++++++ content/docs/troubleshooting.txt | 10 ++++ content/docs/tutorial.txt | 96 ++++++++++++++++++++++++++++++++ content/docs/usage.txt | 10 ++++ content/index.txt | 8 +-- layouts/default.rhtml | 14 +++-- layouts/simple.haml | 54 ++++++++++++++++++ 17 files changed, 492 insertions(+), 11 deletions(-) create mode 100644 content/docs/clients.html create mode 100644 content/docs/controllers.html create mode 100644 content/docs/databases.txt create mode 100644 content/docs/getting_started.txt create mode 100644 content/docs/index.txt create mode 100644 content/docs/installation.txt create mode 100644 content/docs/introduction.html create mode 100644 content/docs/rack.txt create mode 100644 content/docs/routes.html create mode 100644 content/docs/troubleshooting.txt create mode 100644 content/docs/tutorial.txt create mode 100644 content/docs/usage.txt create mode 100644 layouts/simple.haml diff --git a/Rakefile b/Rakefile index 9319591..acf1147 100644 --- a/Rakefile +++ b/Rakefile @@ -13,8 +13,8 @@ namespace(:site) do desc 'Update the website' task :update => [:build] do - `rsync -avz ./output/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/test/ > /dev/null` - puts "* uploaded ./output/ to http://halcyon.rubyforge.org/test/" + `rsync -avz ./output/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/ > /dev/null` + puts "* uploaded ./output/ to http://halcyon.rubyforge.org/" end end diff --git a/content/css/styles.sass b/content/css/styles.sass index 580a13d..446a3ce 100644 --- a/content/css/styles.sass +++ b/content/css/styles.sass @@ -1,9 +1,19 @@ --- extension: css +layout: none filter: - erb - sass --- +p#notice + # :display none + :background #F6F9FB + :border 1px solid #E1E1E1 + :padding 10px 10px 15px 10px + + strong + :color #DD0000 + /* Created by Igor Penjivrag (www.colorlightstudio.com) - 12.11.2006 Modified by Matt Todd (maraby.org) for personal use. Converted to Sass by Matt Todd (maraby.org). diff --git a/content/docs/clients.html b/content/docs/clients.html new file mode 100644 index 0000000..3d6cda5 --- /dev/null +++ b/content/docs/clients.html @@ -0,0 +1,10 @@ +--- +title: Docs — Customizing Clients +layout: simple +filter: + - erb + - textile +--- +h2. Customizing Clients + +Coming soon. diff --git a/content/docs/controllers.html b/content/docs/controllers.html new file mode 100644 index 0000000..c8208e2 --- /dev/null +++ b/content/docs/controllers.html @@ -0,0 +1,10 @@ +--- +title: Docs — Writing Controllers +layout: simple +filter: + - erb + - textile +--- +h2. Writing Controllers + +Coming soon. diff --git a/content/docs/databases.txt b/content/docs/databases.txt new file mode 100644 index 0000000..c492440 --- /dev/null +++ b/content/docs/databases.txt @@ -0,0 +1,10 @@ +--- +title: Docs — Connecting to Databases +layout: simple +filter: + - erb + - textile +--- +h2. Connecting to Databases + +Coming soon. diff --git a/content/docs/getting_started.txt b/content/docs/getting_started.txt new file mode 100644 index 0000000..f9e04e2 --- /dev/null +++ b/content/docs/getting_started.txt @@ -0,0 +1,28 @@ +--- +title: Docs — Getting Started +layout: simple +filter: + - erb + - textile +--- +h2. Introduction + +Sample documentation page. + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class QueueController < Application + @queue = [] + def enqueue + @queue << params[:body] + ok + end + def dequeue + ok @queue.shift + end + def list + ok @queue + end +end +<% end -%> + +
    $ halcyon start -p 4647
    diff --git a/content/docs/index.txt b/content/docs/index.txt new file mode 100644 index 0000000..a6352d8 --- /dev/null +++ b/content/docs/index.txt @@ -0,0 +1,40 @@ +--- +title: Docs +layout: simple +filter: + - erb + - textile +--- +h2. Documentation + +If you're looking for thorough documentation and plenty of samples, this is where you will find what you're looking for. + +Links marked with a -strike- are not finished. Bear with us while we grind away at the documentation. + + +h3. Getting Started + +* -"Introduction to Halcyon":/docs/introduction.html- +* "Installation":/docs/installation.html +* "Tutorial":/docs/tutorial.html + + +h3. Advanced Topics + +* "Defining Routes":/docs/routes.html +* -"Writing Controllers":/docs/controllers.html- +* -"Connecting to Databases":/docs/databases.html- +* -"Customizing Clients":/docs/clients.html- +* "Advanced Rack Topics":/docs/rack.html +* -"Troubleshooting":/docs/troubleshooting.html- + + +h3. Samples + +* -"Simple":/docs/samples/simple.html- +* -"MPQueue":/docs/samples/mpqueue.html- + + +h3. Manual + +* "The Halcyon RDocs":/manual/ diff --git a/content/docs/installation.txt b/content/docs/installation.txt new file mode 100644 index 0000000..226ea98 --- /dev/null +++ b/content/docs/installation.txt @@ -0,0 +1,66 @@ +--- +title: Docs — Installation +layout: simple +filter: + - erb + - textile +--- +h2. Installation + +There are three primary ways to install Halcyon, from RubyGems, from the repository, and from the tarball downloaded from the repository. + + +h3. Dependencies + +Before showing you how to install Halcyon, here's a quick review of the primary Halcyon dependencies. + +* "Rack":http://rack.rubyforge.org/ -- Handles serving Halcyon Apps +* "JSON":http://json.rubyforge.org/ -- Renders and Parses JSON strings +* "Merb":http://merbivore.com/ -- Provides many core language extensions and a clean Router implementation +* "RubiGen":http://rubigen.rubyforge.org/ -- Handles generating fresh Halcyon Apps + + +h3. RubyGems + +To install Halcyon from RubyGems, run this at your command line: + +
    $ sudo gem install halcyon
    + + +h3. From Sources + +In order to install directly from the source, you will either need to install Git (available via Apt, Yum, or MacPorts), or look at the section below on installing from the Tarball. + +The "primary development repository":http://github.com/mtodd/halcyon.git is located at "GitHub":http://github.com/. + +Make sure you're in an appropriate directory (~/source/ perhaps) and run this command from your command line: + +
    $ git clone git://github.com/mtodd/halcyon.git
    + +This will download the repository into @halcyon/@. Change into this directory and run this command: + +
    $ rake install
    + +This will package up Halcyon into a Gem package and then install it locally. + + +h3. From Source Tarball + +The tarball is provided by the same location as the "Git repository":http://github.com/mtodd/halcyon. + +You can either go to that website and click _Download Tarball_ or run this command (replacing @curl -O@ with @wget@ if you choose): + +
    $ curl -o halcyon.tgz http://github.com/tarballs/mtodd-halcyon-master.tar.gz
    + +Once done, unpack the tarball and change into the created directory: + +
    
    +$ tar xzf halcyon.tgz
    +$ cd mtodd-halcyon-master/
    +
    + +Once your in the directory, run the following: + +
    $ rake install
    + +This will perform the same was as from source. diff --git a/content/docs/introduction.html b/content/docs/introduction.html new file mode 100644 index 0000000..bfc34b2 --- /dev/null +++ b/content/docs/introduction.html @@ -0,0 +1,10 @@ +--- +title: Docs — Introduction to Halcyon +layout: simple +filter: + - erb + - textile +--- +h2. Introduction to Halcyon + +Coming soon. diff --git a/content/docs/rack.txt b/content/docs/rack.txt new file mode 100644 index 0000000..f323d5b --- /dev/null +++ b/content/docs/rack.txt @@ -0,0 +1,54 @@ +--- +title: Docs — Advanced Rack Topics +layout: simple +filter: + - erb + - textile +--- +h2. Advanced Rack Topics + +Coming soon! + +Rack is a meta-framework used to simplify servers and frameworks/applications communicating. Read some interesting bits in this InfoQ article "Rack: HTTP request handling made easy":http://www.infoq.com/news/2008/04/rack-http-web. + + +h3. Using Halcyon to back web apps + +Since Halcyon apps are simply Rack applications, using Halcyon selectively behind your larger web application can be handy. For example, your Halcyon application can map to @/api/...@ and provide a public API to your fancy application. + +In order to do this, a Rack middleware will need to be defined in order to make sure that requests get routed to both your web application and your Halcyon application depending on the correct request URL. There are two primary ways to do this: + +# Look for a specific URL pattern and route to your Halcyon app, failing over to your web app; or +# Use the Cascade middleware already built into Rack which tries each app in its array until it finds an app that doesn't throw back a _404 Not Found_ error. + +Both of these are simple and effective methods, so both will be covered. + + +h4. Custom Middleware + +Look at Ezra Zygmuntowicz' MountainWest Ruby Conf video for a good example, around 18 minutes in. + +_Coming soon!_ + + +h4. Using @Cascade@ + +Refer to "Rack::Cascade":http://rack.rubyforge.org/doc/classes/Rack/Cascade.html. + +_Coming soon!_ + + +h3. Writing Custom Middleware + +"Vidar Hokstad":http://www.hokstad.com/tag/rack has several great Rack middleware examples you should check out. + +_Coming soon!_ + + +h3. Useful Rack Middleware + +This list will contain articles on and links to Rack middleware that you may find interesting or useful: + +* "Rewriting Content-Types with Rack":http://www.hokstad.com/rewriting-content-types-with-rack.html +* "Adding Cache Headers":http://www.hokstad.com/rack-middleware-adding-cache-headers.html +* "Tracking Referrers":http://www.hokstad.com/latest-referrers-using-rack-and-ruby.html diff --git a/content/docs/routes.html b/content/docs/routes.html new file mode 100644 index 0000000..df636b0 --- /dev/null +++ b/content/docs/routes.html @@ -0,0 +1,69 @@ +--- +title: Docs — Defining Routes +layout: simple +filter: + - erb + - textile +--- +h2. Defining Routes + +One of the most peculiar parts of Halcyon is its dependency on "Merb":http://merbivore.com/, but this is for a very good reason: Merb provides a great deal of great code that is modular and clean, perfect to implement into Halcyon. This has two affects: first, those pieces of code are very well documented by a very large and active community, and secondly is that they are continually being updated to better perform. Rewriting what Merb has already done would be silly. *So when it comes to defining routes in Halcyon, much of the documentation for defining routes in Merb still applies!* + +For links to various Routing documentation for Merb, jump to the bottom of the page and look under the Resources section. + + +h3. Getting Started + +Routes are defined in @app_name/config/initialize.rb@, wherein you will find something like this by default: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +# = Required Libraries +%w().each {|dep|require dep} + +# = Initialization +class Halcyon::Application + + startup do |config| + self.logger.info 'Initialize application resources and define routes in config/initialize.rb' + end + + # = Routes + route do |r| + r.match('/time').to(:controller => 'application', :action => 'time') + + r.match('/').to(:controller => 'application', :action => 'index') + + # failover + {:action => 'not_found'} + end + +end +<% end -%> + +In the lower half you see where two routes are defined and one failover route is specified. (This failover route is actually set by default, but it is provided here as well to indicate how to update this default easily). + +Within the @route@ block, @r@ is used to define what routes to match against and where to route those requests to. Routes can be very specific or very general, accepting no or many variables in the route itself. Here are several examples to hopefully clarify the flexibility of these routes: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +r.match('/api/:version/app_name/:controller/:action').to() +r.match('/:controller/:action/:id').to() +r.match('/:controller/:action').to() +r.match('/time').to(:controller => 'utilities', :action => 'time') +r.match('/').to(:controller => 'application', :action => 'usage') +<% end -%> + +The @default_routes@ method is also one provided for by Merb and can also provide extra functionality as well as clarifies some other useful methods like @defer_to@ for conditional routes. Read below in the Resources section for more information. + +_More coming soon!_ + + +h3. Resources + +Merb's API provides two very useful resources for defining routes. These two are the methods used to match paths and define how to handle those routes. These links are: + +* "Merb::Router::Behavior#match":http://merbivore.com/documentation/merb-core/head/index.html?a=M000787&name=match +* "Merb::Router::Behavior#to":http://merbivore.com/documentation/merb-core/head/index.html?a=M000790&name=to + +The @Merb::Router::Behavior@ class is used to generate each route, which you will recognize it as the block parameter passed and used similar to @r.match('/').to(:controller => 'application', :action => 'index')@. + +Also, the "Merb::Router::Behavior#default_routes":http://merbivore.com/documentation/merb-core/head/index.html?a=M000792&name=default_routes method may be worth investigating as it handles defining common routes like @/:controller/:action/:id@ and the like. diff --git a/content/docs/troubleshooting.txt b/content/docs/troubleshooting.txt new file mode 100644 index 0000000..c2132ff --- /dev/null +++ b/content/docs/troubleshooting.txt @@ -0,0 +1,10 @@ +--- +title: Docs — Troubleshooting +layout: simple +filter: + - erb + - textile +--- +h2. Troubleshooting + +Coming soon. diff --git a/content/docs/tutorial.txt b/content/docs/tutorial.txt new file mode 100644 index 0000000..4555d7d --- /dev/null +++ b/content/docs/tutorial.txt @@ -0,0 +1,96 @@ +--- +title: Docs — Tutorial +layout: simple +filter: + - erb + - textile +--- +h2. Tutorial + +Coming to a new, unfamiliar framework can be daunting, especially with nobody there to hold your hand through the scary bits. Hopefully this tutorial will get you through those parts just fine and get you into developing cool services. + + +h3. Installation + +If you've not installed Halcyon yet, read the "Installation":/docs/installation.html guide. + + +h3. Starting a new application + +If you're familiar with "Rails":http://rubyonrails.org/ or "Merb":http://merbivore.com/, you know that you can begin working on a new application very easily by issuing a simple command, similar to @rails app_name@. Halcyon provides a similar command to do the same. + +Run the following in your command line (make sure you're in a directory you're OK having your project created in): + +
    $ halcyon init app_name
    + +This will generate output similar to the following: + +
    
    +      create
    +      create  runner.ru
    +      create  README
    +      create  config
    +      create  config/initialize.rb
    +      create  config/config.yml
    +      create  app
    +      create  app/application.rb
    +      create  Rakefile
    +      create  lib
    +      create  lib/client.rb
    +      create  log
    +
    + +This shows you what files were created, but more importantly, what files you'll be working with. + +Now, change into the @app_name@ directory: + +
    $ cd app_name
    + +You are now the proud owner of a brand new Halcyon application. Now would be a good time to run @git init@ to begin tracking your app under Git's revision control. + + +h3. Running Halcyon Apps + +So with our brand new application, let's see what running our application looks like: + +
    halcyon start -p 4647
    + +This tells Halcyon to start up the Halcyon application using either Rack's @rackup@ utility or Thin's @thin start@ utility (if "Thin":http://code.macournoyer.com/thin/ is installed) along with the port to run the server on. + +You will see the following output: + +
    
    +(Starting in /path/to/app_name)
    +DEBUG [2008-04-04 04:20:19] (11421) AppName :: Init: Initialize
    +DEBUG [2008-04-04 04:20:19] (11421) AppName :: Load: Application Controller
    + INFO [2008-04-04 04:20:19] (11421) AppName :: Starting up...
    + INFO [2008-04-04 04:20:19] (11421) AppName :: Initialize application resources and define routes in config/initialize.rb
    +DEBUG [2008-04-04 04:20:19] (11421) AppName :: Starting GC.
    + INFO [2008-04-04 04:20:19] (11421) AppName :: Started. PID is 11421
    +
    + +This reveals a bit about its booting process and lets you know when it's ready to begin accepting connections. + +In another shell window, keeping your Halcyon app running, run the following: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +$ irb -r rubygems -r halcyon +>> client = Halcyon::Client.new('http://localhost:4647/') +=> # +>> client.get('/time') +=> {"status"=>200, "body"=>"Fri Apr 04 04:24:15 -0400 2008"} +>> exit +<% end -%> + +What this does is, after requiring the Halcyon library, we create an instance of @Halcyon::Client@, telling it where to connect to. + +After the client is created, we can then perform requests using standard HTTP request types, GET, POST, PUT, and DELETE. Here we simply call @get('/time')@ which gets routed to the @time@ action inside of the @Application@ controller inside of @app_name/app/application.rb@. + +Don't worry, you'll be able to wrap up @get@s and @post@s in your own custom client methods and make corresponding actions in the actual Halcyon application. + + +h2. What's Next + +Now that you know how to get things running, you'll want to customize your application by "Defining Routes":/docs/routes.html, "Writing Controllers":/docs/controllers.html, and "Customizing Clients":/docs/clients.html. + +Still confused? Read a more thorough "Introduction to Halcyon":/docs/introduction.html. diff --git a/content/docs/usage.txt b/content/docs/usage.txt new file mode 100644 index 0000000..2f26666 --- /dev/null +++ b/content/docs/usage.txt @@ -0,0 +1,10 @@ +--- +title: Docs — Usage +layout: simple +filter: + - erb + - textile +--- +h2. Usage + +Coming soon. diff --git a/content/index.txt b/content/index.txt index a3cb227..8b5f78f 100644 --- a/content/index.txt +++ b/content/index.txt @@ -39,12 +39,12 @@ end You can then run it with -$ thin start -r runner.ru -p 4647 +
    $ halcyon start -p 4647
    That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. -Read the "Getting Started Tutorial":#. - +Read the "Docs":/test/docs/. + h2. Supported Platforms @@ -57,6 +57,6 @@ Ohloh's pretty cool and we use it to track the development metrics of Halcyon. C For your viewing pleasure, here are some of the Ohloh project factoids: - + Note: It says that Halcyon is mostly written in Java because we include the client libraries in the code base, each of which can have their own extensive dependencies, including the Java one. Since Ruby's syntax is much leaner, Java's line count overtakes the Ruby line count even though the Java code is only for the client library. diff --git a/layouts/default.rhtml b/layouts/default.rhtml index 72ffb0d..328b8cb 100644 --- a/layouts/default.rhtml +++ b/layouts/default.rhtml @@ -7,13 +7,13 @@ filter: !!! 1.0 Strict %html{:xmlns => 'http://www.w3.org/1999/xhtml', :"xml:lang" => 'en', :lang => 'en'} %head - %title Halcyon + %title Halcyon — <%= @page.title %> %meta{:name => 'content-type', :content => 'text/html;charset=utf-8'} %meta{:name => 'author', :content => 'Igor Pengivrag (www.colorlightstudio.com)'} %meta{:name => 'description', :content => 'Halcyon, Ruby JSON App Framework'} %meta{:name => 'keywords', :content => 'ruby, json, framework, soa, http'} - %link{:rel => 'stylesheet', :type => 'text/css', :href => 'css/styles.css', :media => 'screen'} - %link{:rel => 'stylesheet', :type => 'text/css', :href => 'css/coderay.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => '/css/styles.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => '/css/coderay.css', :media => 'screen'} %body #wrap #top @@ -24,14 +24,18 @@ filter: %li %a.current{:href => '/'} Home %li - %a{:href => '/doc/'} RDoc + %a{:href => '/docs/'} Docs %li %a{:href => 'http://ohloh.net/projects/10313?p=Halcyon'} Ohloh %li %a{:href => 'http://github.com/mtodd/halcyon'} GitHub #content + %p#notice + %strong Notice: + The website is currently being udpated. Sorry for any inconvenience. + #left - =@content + - puts @content #right .box diff --git a/layouts/simple.haml b/layouts/simple.haml new file mode 100644 index 0000000..07d6311 --- /dev/null +++ b/layouts/simple.haml @@ -0,0 +1,54 @@ +--- +extension: html +filter: + - erb + - haml +--- +!!! 1.0 Strict +%html{:xmlns => 'http://www.w3.org/1999/xhtml', :"xml:lang" => 'en', :lang => 'en'} + %head + %title Halcyon — <%= @page.title %> + %meta{:name => 'content-type', :content => 'text/html;charset=utf-8'} + %meta{:name => 'author', :content => 'Igor Pengivrag (www.colorlightstudio.com)'} + %meta{:name => 'description', :content => 'Halcyon, Ruby JSON App Framework'} + %meta{:name => 'keywords', :content => 'ruby, json, framework, soa, http'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => '/css/styles.css', :media => 'screen'} + %link{:rel => 'stylesheet', :type => 'text/css', :href => '/css/coderay.css', :media => 'screen'} + %body + #wrap + #top + %h2 + %a{:href => '/', :title => 'Home'} Halcyon + #menu + %ul + %li + %a.current{:href => '/'} Home + %li + %a{:href => '/docs/'} Docs + %li + %a{:href => 'http://ohloh.net/projects/10313?p=Halcyon'} Ohloh + %li + %a{:href => 'http://github.com/mtodd/halcyon'} GitHub + #content + %p#notice + %strong Notice: + The website is currently being udpated. Sorry for any inconvenience. + + - puts @content + + #clear + + #footer + %p Halcyon © 2007-2008 Matt Todd. Design by Color Light Studio. + %p.small + == Last updated: #{Time.now.strftime('%d %b %Y')} + + %script{:type => 'text/javascript'} + var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); + document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); + %script{:type => 'text/javascript'} + var pageTracker = _gat._getTracker("UA-101852-3"); + pageTracker._initData(); + pageTracker._trackPageview(); + %script{:type => 'text/javascript', :src => 'http://twitter.com/javascripts/blogger.js'} + %script{:type => 'text/javascript', :src => 'http://twitter.com/statuses/user_timeline/halcyon_dev.json?callback=twitterCallback2&count=5'} From d3b19b743411f18d2193b6d152a37532fcf95c0b Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Mon, 7 Apr 2008 05:01:10 -0400 Subject: [PATCH 08/27] Extended, clarified, cleaned up, and started. --- content/docs/index.txt | 2 +- content/docs/introduction.html | 107 ++++++++++++++++++++++++++++++++- content/docs/tutorial.txt | 9 ++- content/index.txt | 40 ++++++++---- 4 files changed, 143 insertions(+), 15 deletions(-) diff --git a/content/docs/index.txt b/content/docs/index.txt index a6352d8..f7fb4fb 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -14,7 +14,7 @@ Links marked with a -strike- are not finished. Bear with us while we grind away h3. Getting Started -* -"Introduction to Halcyon":/docs/introduction.html- +* "Introduction to Halcyon":/docs/introduction.html * "Installation":/docs/installation.html * "Tutorial":/docs/tutorial.html diff --git a/content/docs/introduction.html b/content/docs/introduction.html index bfc34b2..378fe54 100644 --- a/content/docs/introduction.html +++ b/content/docs/introduction.html @@ -7,4 +7,109 @@ --- h2. Introduction to Halcyon -Coming soon. +Halcyon is a simple framework to ease development of service-oriented applications, such as public or private APIs or custom services for applications. + + +h3. Conception + +Halcyon started off as a centralized authentication system for numerous applications on varied platforms, at the time called Aurora. The decision was made to split Aurora into a framework and an application, Halcyon becoming the framework. + + +h3. The Framework + +As a framework, Halcyon breaks your application code up into controllers with absolutely no views and no predefined system for models or database connectivity. Routes are used to define what paths are handled by what actions in which controllers. + + +h4. Controllers + +Halcyon's controllers all inherit from Halcyon::Controller which provides several useful methods for responding in different situations, such as the @ok@ method to respond with the @200 OK@ standard HTTP success response, along with any data you need to send back. + +For example, a controller may look like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def new + # respond with fields acceptable + ok [:body, :title, :tags] + end + def create + case method + when :post + DB[:messages] << params.merge(:tags => params[:tags].join) + ok + when :get + # the params required for new messages + ok [:message, :tags] + else + raise NotImplemented + end + end + def read + ok DB[:messages][params[:id]] + end + def update + case method + when :post + DB[:messages].filter(:id => params[:id]).update(params) + ok + else + raise NotImplemented + end + end + def delete + DB[:messages].filter(:id => params[:id]).delete + ok + end +end +<% end -%> + +@DB@ refers to a "Sequel":http://code.google.com/p/ruby-sequel/ connection, which lets us talk to the @messages@ table. This could just as easily be a Sequel model, ActiveRecord model, or DataMapper model. + +Read more about "Writing Controllers":/docs/controllers.html + + +h4. Routes + +Part of developing a Halcyon app is writing the controllers, but requests need to be routed to the appropriate actions. + +There are, by default, no routes defined for an application, but there is a way to quickly define routes as matching any variation of @/:controller/:action/:id@, etc. For example: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +Halcyon::Application.route do |r| + r.default_routes +end +<% end -%> + +Of course, you can add to the default routes with custom routes, like so: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +Halcyon::Application.route do |r| + r.match('/api/:version/:controller/:action(/:id)?').to() + r.default_routes +end +<% end -%> + +Read more about "Defining Routes":/docs/routes.html. + + +h4. Clients + +The easiest way to communicate with your Halcyon application is with a Halcyon client. By default, it creates a simple way to perform GET, POST, PUT, and DELETE requests on application routes, but can be extended with methods that easily corresponds with your routes. For example: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +>> class MessageClient < Halcyon::Client +*> def create(params) +*> post("/api/1.0/messages/create", params) +*> end +*> end +>> Message = MessageClient.new('http://localhost:4647/') +=> # +>> Message.create(:message => 'First test.', :tags => ['test', 'first']) +=> {'status' => 200, 'body' => {:id => 1}} +>> Message.post("/api/1.0/messages/create", :message => 'Second test.', :tags => []) +=> {'status' => 200, 'body' => {:id => 2}} +<% end -%> + +You can also implement clients into your currently existing models. + +Read more about "Customizing Clients":/docs/clients.html diff --git a/content/docs/tutorial.txt b/content/docs/tutorial.txt index 4555d7d..37d2182 100644 --- a/content/docs/tutorial.txt +++ b/content/docs/tutorial.txt @@ -86,11 +86,16 @@ What this does is, after requiring the Halcyon library, we create an instance of After the client is created, we can then perform requests using standard HTTP request types, GET, POST, PUT, and DELETE. Here we simply call @get('/time')@ which gets routed to the @time@ action inside of the @Application@ controller inside of @app_name/app/application.rb@. -Don't worry, you'll be able to wrap up @get@s and @post@s in your own custom client methods and make corresponding actions in the actual Halcyon application. +Don't worry, you'll be able to wrap up @get@ and @post@ requests in your own custom client methods and make corresponding actions in the actual Halcyon application. + + +h3. Modifying Your App + +_Coming soon!_ h2. What's Next -Now that you know how to get things running, you'll want to customize your application by "Defining Routes":/docs/routes.html, "Writing Controllers":/docs/controllers.html, and "Customizing Clients":/docs/clients.html. +Now that you know how to get things running, you'll want to delve deeper into learning just how to customize your application by reading "Defining Routes":/docs/routes.html, "Writing Controllers":/docs/controllers.html, and "Customizing Clients":/docs/clients.html. Still confused? Read a more thorough "Introduction to Halcyon":/docs/introduction.html. diff --git a/content/index.txt b/content/index.txt index 8b5f78f..7c53cd5 100644 --- a/content/index.txt +++ b/content/index.txt @@ -22,28 +22,46 @@ h2. Functionality & Performance With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> -class QueueController < Application - @queue = [] - def enqueue - @queue << params[:body] - ok +class Messages < Application + def new + # respond with fields acceptable + ok [:body, :title, :tags] + end + def create + case method + when :post + DB[:messages] << params + ok + else + raise NotImplemented + end end - def dequeue - ok @queue.shift + def read + ok DB[:messages][params[:id]] end - def list - ok @queue + def update + case method + when :post + DB[:messages].filter(:id => params[:id]).update(params) + ok + else + raise NotImplemented + end + end + def delete + DB[:messages].filter(:id => params[:id]).delete + ok end end <% end -%> -You can then run it with +You can then run it with:
    $ halcyon start -p 4647
    That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. -Read the "Docs":/test/docs/. +Read the "Docs":/docs/. h2. Supported Platforms From 093302acc0ccd33da79cda819e58e910c4ff1a72 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Wed, 9 Apr 2008 05:04:13 -0400 Subject: [PATCH 09/27] Moved around content from the intro to the tutorial and added more appropriate content to the intro. --- content/docs/introduction.html | 133 ++++++++++----------------------- content/docs/tutorial.txt | 87 ++++++++++++++++++++- 2 files changed, 126 insertions(+), 94 deletions(-) diff --git a/content/docs/introduction.html b/content/docs/introduction.html index 378fe54..ac1be1d 100644 --- a/content/docs/introduction.html +++ b/content/docs/introduction.html @@ -19,97 +19,44 @@ As a framework, Halcyon breaks your application code up into controllers with absolutely no views and no predefined system for models or database connectivity. Routes are used to define what paths are handled by what actions in which controllers. +Halcyon distinguishes itself by the distinct combination of technologies it employs to simplify its task. To clarify, Halcyon specifically chooses to reinvent as little as possible and yet provide a great deal of functionality. We do this by using standard HTTP protocols, transferring complex data across platforms with JSON, designing Halcyon apps on top of "Rack":http://rack.rubyforge.org/, and even reusing portions of "Merb":http://merbivore.com/ to prevent duplication. Here's a quick look at each of these. + + +h4. JSON + +JSON, or JavaScript Object Notation, is a simple format to transport complex data structures across the HTTP medium and across to many platforms. It's a simple format like XML, easy to read and write (unlike XML), and serializable. "CouchDB":http://couchdb.com/ chose JSON over XML for similar reasons, and has been very happy with the decision. + + + +h4. HTTP + +Hypertext Transfer Protocol is a widely support protocol, supported on pretty much every platform, and provides a familiar way to modify resources with Representational State Transfer. + + +h4. Rack + +Rack provides a uniform model for handling HTTP request and response cycles, allowing for server-independent development and for powerful layering of applications and specialized functionality through middleware. + +Jim Weirich's talk at MountainWest 2008 focuses on the power of simplicity. Specifically, Jim mentions three poignant parts to a powerful system, having: + +# Small Core +# Simple Rules +# Powerful Abstractions + +Rack's design elegantly shows off the power of its simplicity. + + +h4. Merb + +Having Merb as a dependency of Halcyon seems a bit ironic, but its active community, quality modular code, and thorough documentation provides an excellent souce of functionality without all of the repetitive development. Halcyon specifically takes advantage of the Merb Router and the Core Extensions. + + +h3. Purpose + +... + + +h3. Alternatives + +... -h4. Controllers - -Halcyon's controllers all inherit from Halcyon::Controller which provides several useful methods for responding in different situations, such as the @ok@ method to respond with the @200 OK@ standard HTTP success response, along with any data you need to send back. - -For example, a controller may look like this: - -<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> -class Messages < Application - def new - # respond with fields acceptable - ok [:body, :title, :tags] - end - def create - case method - when :post - DB[:messages] << params.merge(:tags => params[:tags].join) - ok - when :get - # the params required for new messages - ok [:message, :tags] - else - raise NotImplemented - end - end - def read - ok DB[:messages][params[:id]] - end - def update - case method - when :post - DB[:messages].filter(:id => params[:id]).update(params) - ok - else - raise NotImplemented - end - end - def delete - DB[:messages].filter(:id => params[:id]).delete - ok - end -end -<% end -%> - -@DB@ refers to a "Sequel":http://code.google.com/p/ruby-sequel/ connection, which lets us talk to the @messages@ table. This could just as easily be a Sequel model, ActiveRecord model, or DataMapper model. - -Read more about "Writing Controllers":/docs/controllers.html - - -h4. Routes - -Part of developing a Halcyon app is writing the controllers, but requests need to be routed to the appropriate actions. - -There are, by default, no routes defined for an application, but there is a way to quickly define routes as matching any variation of @/:controller/:action/:id@, etc. For example: - -<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> -Halcyon::Application.route do |r| - r.default_routes -end -<% end -%> - -Of course, you can add to the default routes with custom routes, like so: - -<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> -Halcyon::Application.route do |r| - r.match('/api/:version/:controller/:action(/:id)?').to() - r.default_routes -end -<% end -%> - -Read more about "Defining Routes":/docs/routes.html. - - -h4. Clients - -The easiest way to communicate with your Halcyon application is with a Halcyon client. By default, it creates a simple way to perform GET, POST, PUT, and DELETE requests on application routes, but can be extended with methods that easily corresponds with your routes. For example: - -<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> ->> class MessageClient < Halcyon::Client -*> def create(params) -*> post("/api/1.0/messages/create", params) -*> end -*> end ->> Message = MessageClient.new('http://localhost:4647/') -=> # ->> Message.create(:message => 'First test.', :tags => ['test', 'first']) -=> {'status' => 200, 'body' => {:id => 1}} ->> Message.post("/api/1.0/messages/create", :message => 'Second test.', :tags => []) -=> {'status' => 200, 'body' => {:id => 2}} -<% end -%> - -You can also implement clients into your currently existing models. - -Read more about "Customizing Clients":/docs/clients.html diff --git a/content/docs/tutorial.txt b/content/docs/tutorial.txt index 37d2182..6e36aa9 100644 --- a/content/docs/tutorial.txt +++ b/content/docs/tutorial.txt @@ -91,7 +91,92 @@ Don't worry, you'll be able to wrap up @get@ and @post@ requests in your own cus h3. Modifying Your App -_Coming soon!_ + +h4. Controllers + +Halcyon's controllers all inherit from Halcyon::Controller which provides several useful methods for responding in different situations, such as the @ok@ method to respond with the @200 OK@ standard HTTP success response, along with any data you need to send back. + +For example, a controller may look like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def new + # respond with fields acceptable + ok [:body, :title, :tags] + end + def create + case method + when :post + DB[:messages] << params.merge(:tags => params[:tags].join) + ok + when :get + # the params required for new messages + ok [:message, :tags] + else + raise NotImplemented + end + end + def read + ok DB[:messages][params[:id]] + end + def update + case method + when :post + DB[:messages].filter(:id => params[:id]).update(params) + ok + else + raise NotImplemented + end + end + def delete + DB[:messages].filter(:id => params[:id]).delete + ok + end +end +<% end -%> + +@DB@ refers to a "Sequel":http://code.google.com/p/ruby-sequel/ connection, which lets us talk to the @messages@ table. This could just as easily be a Sequel model, ActiveRecord model, or DataMapper model. + +Read more about "Writing Controllers":/docs/controllers.html + + +h4. Routes + +Part of developing a Halcyon app is writing the controllers, but requests need to be routed to the appropriate actions. + +There are, by default, no routes defined for an application, but there is a way to quickly define routes as matching any variation of @/:controller/:action/:id@, etc. Here is a sample, including a custom route as well: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +Halcyon::Application.route do |r| + r.match('/api/:version/:controller/:action(/:id)?').to() + r.default_routes +end +<% end -%> + +Read more about "Defining Routes":/docs/routes.html. + + +h4. Clients + +The easiest way to communicate with your Halcyon application is with a Halcyon client. By default, it creates a simple way to perform GET, POST, PUT, and DELETE requests on application routes, but can be extended with methods that easily corresponds with your routes. For example: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +>> class MessageClient < Halcyon::Client +*> def create(params) +*> post("/api/1.0/messages/create", params) +*> end +*> end +>> Message = MessageClient.new('http://localhost:4647/') +=> # +>> Message.create(:message => 'First test.', :tags => ['test', 'first']) +=> {'status' => 200, 'body' => {:id => 1}} +>> Message.post("/api/1.0/messages/create", :message => 'Second test.', :tags => []) +=> {'status' => 200, 'body' => {:id => 2}} +<% end -%> + +You can also implement clients into your currently existing models. + +Read more about "Customizing Clients":/docs/clients.html h2. What's Next From d64c132253bae4a1374552e222def89c7a8ee131 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Wed, 16 Apr 2008 18:16:40 -0400 Subject: [PATCH 10/27] Added link and badge to RubyFringe. --- layouts/default.rhtml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/layouts/default.rhtml b/layouts/default.rhtml index 328b8cb..10951e7 100644 --- a/layouts/default.rhtml +++ b/layouts/default.rhtml @@ -35,9 +35,18 @@ filter: The website is currently being udpated. Sorry for any inconvenience. #left + - puts @content #right + + #rubyfringe{:style => 'margin:0 1em 1em;'} + %a{:href => 'http://rubyfringe.com/'} + %img{:src => 'http://halcyon.rubyforge.org/img/rubyfringespeak.jpg', :title => 'Speaking at RubyFringe', :border => '0', :style => 'float: right;'} + I'm speaking at + %a{:href => 'http://rubyfringe.com/'} RubyFringe + about Halcyon in mid-July along with several other notable Ruby developers. RubyFringe is unlike most other Ruby conferences... check it out! + .box %h2 About %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. From 12e1a4de7e4eeb2fac2d9e0a259fce3b1fba6399 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 13 May 2008 06:04:11 -0400 Subject: [PATCH 11/27] Updated list of sample applications to be delved into and added documentation for connecting to databases. --- content/docs/databases.txt | 226 ++++++++++++++++++++++++++++++++++++- content/docs/index.txt | 8 +- 2 files changed, 230 insertions(+), 4 deletions(-) diff --git a/content/docs/databases.txt b/content/docs/databases.txt index c492440..d297e64 100644 --- a/content/docs/databases.txt +++ b/content/docs/databases.txt @@ -7,4 +7,228 @@ filter: --- h2. Connecting to Databases -Coming soon. +Although Halcyon doesn't come with any ORM-specific plumbing, getting connected +and building database-centric Halcyon applications is trivial. Well, it's +certainly not impossible. + +As of Halcyon's 0.5.0 Release (which, as of this writing, will be released any +day now), connecting to databases is surprisingly easy, but requires some +effort. Getting connected to a database is made of a few essential steps: +loading the database configuration, connecting to the database, and, +optionally, loading all models as well as hooking up migrations. + +The instructions provided here will be focused on a +"Sequel":http://code.google.com/p/ruby-sequel system, but the instructions +should still be relevant for most systems, including +"DataMapper":http://datamapper.org/ and "ActiveRecord":http://rubyonrails.org/. + + +h3. But First + +One tiny detail to go ahead and address is that you will need to require the +appropriate ORM library, which can be done from +config/init/requires.rb. You can get by with something like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## config/init/requires.rb +%w(sample_app sequel).each{|dep|require dep} +<% end -%> + +Don't mind the unfamiliar syntax (if, indeed, this is unfamiliar), we're simply +creating an array of strings separated by spaces and then requiring each +dependency programmatically. + +Go ahead and require the sample_app file (which is located in +lib/sample_app.rb file), or, specifically, whatever your +application's primary module located in lib/, we'll put additional +functionality here as well as storing the current instance of the database +connection. + + +h3. Load Database Configuration + +Presently, Halcyon doesn't have any explicit location to load the database, but +it does provide a mechanism for injecting into the initialization process. This +is done by creating a file in the config/init/ folder. This file +will look something like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## config/init/database.rb +Halcyon.db = Halcyon::Runner.load_config(Halcyon.paths[:config]/'database.yml') +Halcyon.db = Halcyon.db[(Halcyon.environment || :development).to_sym] +<% end -%> + +What's above appears fairly verbose, but what's happening is that the file, +located at Halcyon.root/'config'/'database.yml' (which gets +expanded to the application directory's configuration folder) is getting parsed +by the framework configuration loader (through YAML), processed (through Mash), +and then saved into Halcyon.db which gets mapped to +Halcyon.config[:db]. (This is a common abstraction mechanism for +Halcyon configuration values.) + +We're also telling it to select the current application runtime environment's +database configuration. Let's assume a somewhat standard database configuration +file: + +<% coderay(:lang => "yaml", :line_numbers => "inline", :tab_width => 2) do -%> +--- +## config/database.yml +development: &defaults + adapter: mysql + database: sample_development + username: sample_user + password: sample_password + host: localhost + +test: + <<: *defaults + database: sample_test + +production: + <<: *defaults + database: sample_production +<% end -%> + +This should look familiar, and the unfamiliar parts should at least be +comprehendible. + + +h3. Connecting + +Once the database configuration has been loaded, we will need to actually +connect to the database in question. We will put this in the +config/init/hooks.rb file since we want this to happen once the +application has been fully initialized with all of its necessary requirements +preloaded. + +Go ahead and add this to your startup hook: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## config/init/hooks.rb, in Halcyon::Application.startup block +# Connect to DB +SampleApp::DB = Sequel.connect(Halcyon.db) +SampleApp::DB.logger = Halcyon.logger if $DEBUG +logger.info 'Connected to Database' +<% end -%> + +*Note*: Be sure to put this inside of the +Halcyon::Application.startup code block. This is not demonstrated +explicitly above, but is essential. + +Sequel.connect is specific to the Sequel library, but it is fairly +obvious what purpose it servers. We store the result, an instance of a +connection, as SampleApp::DB to have a common location from which +to refer to the connection, particularly under the SampleApp module +to signify its tight bond to the application domain. We also set the logger +instance to the current, app-wide logger if the $DEBUG flag is set. + +While we're at it, let's go ahead and load any models we may have stored in +app/models/, the unofficial default location for application +models. Put this directly below the code listed above inside of +config/init/hooks.rb: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## config/init/hooks.rb, in Halcyon::Application.startup block +# Load Models +Dir.glob([Halcyon.paths[:model]/'*.rb']).each do |model| + logger.debug "Load: #{File.basename(model).chomp('.rb').camel_case} Model" if require model +end +<% end -%> + +The above code simply requires each model file found within the model directory, +printing out a debugging message if it succeeds. Any failure should normally +result in application startup failing (which is expected). + +If you have questions about how to create models with the Sequel ORM, refer to +their excellent +"Sequel Models":http://code.google.com/p/ruby-sequel/wiki/SequelModels wiki page +which contains simple instructions for writing your models. + + +h3. Step One: Done + +And that, in turn, connects your application to a database. + + +h2. Step Two: Polish + +Now, to simplify using this database system, you may be interested in a few +conventional portions of code that will simplify your life, particularly with +keeping track of your database schema with migrations. + + +h3. Migrations + +Most developers interested in Halcyon should be familiar with migrations already +since many come from Rails or more modern frameworks like Merb et al, so we +won't go over them. However, we will learn about the conventions used to +organize and load migrations. + +Migrations are often stored in lib/migrations with the standard +001_create_records.rb naming structure. You can find out the +specific migration syntax to use from the Sequel wiki, linked above, or from the +WeeDB sample application, linked at the bottom of this page. + +Now, to make your application use migrations is the fun part. In the section +labelled for custom Rake tasks in the application Rakefile, place +this code: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## Rakefile +desc "Load up the application environment" +task :env do + $log = '' + $logger = Logger.new(StringIO.new($log)) + Halcyon.config = {:logger => $logger} + Halcyon::Runner.new +end + +namespace(:db) do + desc "Migrate the database to the latest version" + task :migrate => :env do + current = Sequel::Migrator.get_current_migration_version(WeeDB::DB) + latest = Sequel::Migrator.apply(WeeDB::DB, Halcyon.paths[:lib]/'migrations') + puts "Database successfully migrated to latest version (#{latest})." if current < latest + puts "Migrations finished successfully." + end +end +<% end -%> + +The first task, though seemingly unneeded, goes through and loads the full +application environment, including the instance of the database connection. This +is used to check the current migration version and also to apply the migrations +to if necessary. It also hides any normal debugger output, though it keeps it +saved for when it's necessary. + +The second task in the db namespace then executes the migrations if +in fact they are out of date. + +Putting these tasks into the Rakefile opens up more in the future +and prevents cluttering up more of the application loading process. Also, it's +at least somewhat familiar because we can run a command like so: + +
    
    +$ rake db:migrate
    +
    + + +h2. Step Three: Usage + +Accessing data from the database an be trivial, depending on whether you like to +use the models you've defined. If you've created your models already, you +probably have already read the documentation for accessing rows with the models. +Refer there again if you have questions. And, of course, there's always the +WeeDB sample app with actual code for connecting to and manipulating the +database inside of a Halcyon app, specifically in the Records +controller. + + +h2. Conclusion + +Hope this helps you get familiar with how to start using databases with your +Halcyon applications sooner than ever. + +If you're looking for a good example of this code in action, check out the WeeDB +sample application located at the "GitHUB +repository":http://github.com/mtodd/halcyon/tree/master/examples/weedb/ which is +where most of this code was pulled from. diff --git a/content/docs/index.txt b/content/docs/index.txt index f7fb4fb..da60f2b 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -23,7 +23,7 @@ h3. Advanced Topics * "Defining Routes":/docs/routes.html * -"Writing Controllers":/docs/controllers.html- -* -"Connecting to Databases":/docs/databases.html- +* "Connecting to Databases":/docs/databases.html * -"Customizing Clients":/docs/clients.html- * "Advanced Rack Topics":/docs/rack.html * -"Troubleshooting":/docs/troubleshooting.html- @@ -31,8 +31,10 @@ h3. Advanced Topics h3. Samples -* -"Simple":/docs/samples/simple.html- -* -"MPQueue":/docs/samples/mpqueue.html- +* -"Aurora":/docs/samples/aurora.html- +* -"Guesser":/docs/samples/guesser.html- +* -"Ranger":/docs/samples/ranger.html- +* -"WeeDB":/docs/samples/weedb.html- h3. Manual From 9661dbdc645ed6f564c47795aa91d6240f9b01bb Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 27 May 2008 16:05:49 -0400 Subject: [PATCH 12/27] Fixed some of the content on the home page and fixed a version number for Haml in the Rakefile. --- Rakefile | 3 +++ content/about.txt | 1 - content/index.txt | 30 +++++++++++------------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/Rakefile b/Rakefile index acf1147..41907d1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,8 @@ # $Id$ +gem 'haml', '=1.8.2' +require 'haml' + load 'tasks/setup.rb' task :default => :build diff --git a/content/about.txt b/content/about.txt index fcc93d1..2c25786 100644 --- a/content/about.txt +++ b/content/about.txt @@ -8,4 +8,3 @@ filter: p(title). <%= h(@page.title) %> Halcyon is a JSON web app framework built on Rack. - diff --git a/content/index.txt b/content/index.txt index 7c53cd5..8ca49b7 100644 --- a/content/index.txt +++ b/content/index.txt @@ -22,34 +22,26 @@ h2. Functionality & Performance With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Message < Sequel::Model; end class Messages < Application def new # respond with fields acceptable - ok [:body, :title, :tags] + ok Message.columns end def create - case method - when :post - DB[:messages] << params - ok - else - raise NotImplemented - end + msg = Message.create(params) + msg.save # add error handling + ok msg.id end def read - ok DB[:messages][params[:id]] + ok Message[params[:id]] end def update - case method - when :post - DB[:messages].filter(:id => params[:id]).update(params) - ok - else - raise NotImplemented - end + Message.filter(:id => params[:id]).update(params) + ok end def delete - DB[:messages].filter(:id => params[:id]).delete + Message.filter(:id => params[:id]).delete ok end end @@ -68,6 +60,8 @@ h2. Supported Platforms Halcyon is primarily written in Ruby, but Halcyon also supports multiple platforms due to the fact that it communicates via HTTP and packages its messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, with more clients planned. +You can see the various client implementations at http://github.com/mtodd/halcyon-clients (except for the Ruby client, which is part of the Halcyon gem). + h2. Metrics @@ -76,5 +70,3 @@ Ohloh's pretty cool and we use it to track the development metrics of Halcyon. C For your viewing pleasure, here are some of the Ohloh project factoids: - -Note: It says that Halcyon is mostly written in Java because we include the client libraries in the code base, each of which can have their own extensive dependencies, including the Java one. Since Ruby's syntax is much leaner, Java's line count overtakes the Ruby line count even though the Java code is only for the client library. From 1bb55b5d827dadbedbf8c44e7e20f07ee5e6d8a7 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 27 May 2008 19:45:32 -0400 Subject: [PATCH 13/27] Updated documentation, added initial sample WeeDB documentation, etc. --- content/docs/databases.txt | 9 +- content/docs/index.txt | 3 +- content/docs/installation.txt | 5 ++ content/docs/introduction.html | 109 +++++++++++++++++++---- content/docs/responding.txt | 155 +++++++++++++++++++++++++++++++++ content/docs/samples/weedb.txt | 17 ++++ content/index.txt | 9 +- 7 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 content/docs/responding.txt create mode 100644 content/docs/samples/weedb.txt diff --git a/content/docs/databases.txt b/content/docs/databases.txt index d297e64..ef2a8d0 100644 --- a/content/docs/databases.txt +++ b/content/docs/databases.txt @@ -179,15 +179,16 @@ desc "Load up the application environment" task :env do $log = '' $logger = Logger.new(StringIO.new($log)) - Halcyon.config = {:logger => $logger} + Halcyon.config = {:logger => $logger, + :environment => (ENV['HALCYON_ENV'] || ENV['ENV'] || :development).to_sym} Halcyon::Runner.new end - + namespace(:db) do desc "Migrate the database to the latest version" task :migrate => :env do - current = Sequel::Migrator.get_current_migration_version(WeeDB::DB) - latest = Sequel::Migrator.apply(WeeDB::DB, Halcyon.paths[:lib]/'migrations') + current = Sequel::Migrator.get_current_migration_version(SampleApp::DB) + latest = Sequel::Migrator.apply(SampleApp::DB, Halcyon.paths[:lib]/'migrations') puts "Database successfully migrated to latest version (#{latest})." if current < latest puts "Migrations finished successfully." end diff --git a/content/docs/index.txt b/content/docs/index.txt index da60f2b..dee6f0c 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -25,6 +25,7 @@ h3. Advanced Topics * -"Writing Controllers":/docs/controllers.html- * "Connecting to Databases":/docs/databases.html * -"Customizing Clients":/docs/clients.html- +* "Responding to the Client":/docs/responding.html * "Advanced Rack Topics":/docs/rack.html * -"Troubleshooting":/docs/troubleshooting.html- @@ -34,7 +35,7 @@ h3. Samples * -"Aurora":/docs/samples/aurora.html- * -"Guesser":/docs/samples/guesser.html- * -"Ranger":/docs/samples/ranger.html- -* -"WeeDB":/docs/samples/weedb.html- +* "WeeDB":/docs/samples/weedb.html h3. Manual diff --git a/content/docs/installation.txt b/content/docs/installation.txt index 226ea98..6e34400 100644 --- a/content/docs/installation.txt +++ b/content/docs/installation.txt @@ -26,6 +26,11 @@ To install Halcyon from RubyGems, run this at your command line:
    $ sudo gem install halcyon
    +There is also the latest development release available from +http://halcyon.rubyforge.org/latest/. This can be installed with: + +
    $ sudo gem install --source=http://halcyon.rubyforge.org/latest/
    + h3. From Sources diff --git a/content/docs/introduction.html b/content/docs/introduction.html index ac1be1d..8d1674d 100644 --- a/content/docs/introduction.html +++ b/content/docs/introduction.html @@ -5,39 +5,91 @@ - erb - textile --- + h2. Introduction to Halcyon -Halcyon is a simple framework to ease development of service-oriented applications, such as public or private APIs or custom services for applications. +Halcyon is a simple framework to ease development of service-oriented +applications, such as public or private APIs or custom services for +applications. + + +h2. Purpose + +This is the *what is Halcyon for?* question and basically summarizes why anyone +would need to use Halcyon. Halcyon's purpose is to provide a dedicated, +simplistic application development framework for exposing functionality through +HTTP/JSON requests, removing the need for content-negotiation or explicitly +formatting everything to the JSON format. + +h3. An Example -h3. Conception +This can be hard to grasp at immediately, so let's take a look at an example: +"Twitter":http://twitter.com/ provides a service centered around tiny +(140-character limit) messages being aggregated for friends and family. Twitter +also provides a service for making third party clients to send messages to or +read messages from Twitter without having to be on the website. -Halcyon started off as a centralized authentication system for numerous applications on varied platforms, at the time called Aurora. The decision was made to split Aurora into a framework and an application, Halcyon becoming the framework. +Twitter does this by exposing URLs for applications to send data to and pull +data from. The Twitter API (what they've called this interface to their +application functionality) can be seen at +"The Twitter API page":http://groups.google.com/group/twitter-development-talk/web/api-documentation#EasyWay +with several examples of how to use Twitter outside of the website. +For example, to post a new message (or a _status update_ as Twitter calls it), +you would POST the data to http://twitter.com/statuses/update.xml. -h3. The Framework +Twitter responds in XML and expects XML as its input in some cases, whereas +Halcyon applications (for now) just use JSON. JSON is widely accepted and used +across the web and competes with XML. -As a framework, Halcyon breaks your application code up into controllers with absolutely no views and no predefined system for models or database connectivity. Routes are used to define what paths are handled by what actions in which controllers. +Halcyon could just as easily provide an API to a given application, routing +requests to controllers with specific actions without the need to negotiate the +content types requested (application/json) or define views. -Halcyon distinguishes itself by the distinct combination of technologies it employs to simplify its task. To clarify, Halcyon specifically chooses to reinvent as little as possible and yet provide a great deal of functionality. We do this by using standard HTTP protocols, transferring complex data across platforms with JSON, designing Halcyon apps on top of "Rack":http://rack.rubyforge.org/, and even reusing portions of "Merb":http://merbivore.com/ to prevent duplication. Here's a quick look at each of these. +h2. The Framework -h4. JSON +As a framework, Halcyon breaks your application code up into controllers with +absolutely no views and no predefined system for models or database +connectivity. Routes are used to define what paths are handled by what actions +in which controllers. -JSON, or JavaScript Object Notation, is a simple format to transport complex data structures across the HTTP medium and across to many platforms. It's a simple format like XML, easy to read and write (unlike XML), and serializable. "CouchDB":http://couchdb.com/ chose JSON over XML for similar reasons, and has been very happy with the decision. +Halcyon distinguishes itself by the distinct combination of technologies it +employs to simplify its task. To clarify, Halcyon specifically chooses to +reinvent as little as possible and yet provide a great deal of functionality. +We do this by using standard HTTP protocols, transferring complex data across +platforms with JSON, designing Halcyon apps on top of +"Rack":http://rack.rubyforge.org/, and even reusing portions of +"Merb":http://merbivore.com/ to prevent duplication. Here's a quick look at +each of these. +h3. JSON -h4. HTTP +JSON, or JavaScript Object Notation, is a simple format to transport complex +data structures across the HTTP medium and across to many platforms. It's a +simple format like XML, easy to read and write (unlike XML), and serializable. +"CouchDB":http://couchdb.com/ chose JSON over XML for similar reasons, and has +been very happy with the decision. -Hypertext Transfer Protocol is a widely support protocol, supported on pretty much every platform, and provides a familiar way to modify resources with Representational State Transfer. -h4. Rack +h3. HTTP -Rack provides a uniform model for handling HTTP request and response cycles, allowing for server-independent development and for powerful layering of applications and specialized functionality through middleware. +Hypertext Transfer Protocol is a widely support protocol, supported on pretty +much every platform, and provides a familiar way to modify resources with +"Representational State Transfer":http://wikipedia.org/wiki/REST. -Jim Weirich's talk at MountainWest 2008 focuses on the power of simplicity. Specifically, Jim mentions three poignant parts to a powerful system, having: + +h3. Rack + +Rack provides a uniform model for handling HTTP request and response cycles, +allowing for server-independent development and for powerful layering of +applications and specialized functionality through middleware. + +Jim Weirich's talk at MountainWest 2008 focuses on the power of simplicity. +Specifically, Jim mentions three poignant parts to a powerful system, having: # Small Core # Simple Rules @@ -46,17 +98,36 @@ Rack's design elegantly shows off the power of its simplicity. -h4. Merb +h3. Merb + +Having Merb as a dependency of Halcyon seems a bit ironic, but its active +community, quality modular code, and thorough documentation provides an +excellent souce of functionality without all of the repetitive development. +Halcyon specifically takes advantage of the Merb Router and the Core Extensions. -Having Merb as a dependency of Halcyon seems a bit ironic, but its active community, quality modular code, and thorough documentation provides an excellent souce of functionality without all of the repetitive development. Halcyon specifically takes advantage of the Merb Router and the Core Extensions. +h2. Conception -h3. Purpose +Halcyon started off as a centralized authentication system for numerous +applications on varied platforms, at the time called Aurora. The decision was +made to split Aurora into a framework and an application, Halcyon becoming the +framework. -... +h2. Alternatives -h3. Alternatives +Any web application framework should be a sufficient alternative, and may +provide a better solution. Here are several frameworks that could be used +instead of Halcyon, though may require more effort to handle the incoming and +outgoing JSON requests. -... +* "Merb":http://merbivore.com/ +* "Sinatra":http://sinatra.rubyforge.org/ +* "Ramaze":http://ramaze.net/ +* "Mack":http://mackframework.com/ +* "Rack":http://rack.rubyforge.org/ +* "Vintage":http://vintage.devjavu.com/ +* "Rails":http://rubyonrails.org/ +A more comprehensive list can be found at +"the Ramaze website":http://ramaze.net/#other-frameworks. diff --git a/content/docs/responding.txt b/content/docs/responding.txt new file mode 100644 index 0000000..544df28 --- /dev/null +++ b/content/docs/responding.txt @@ -0,0 +1,155 @@ +--- +title: Docs — Responding to the Client +layout: simple +filter: + - erb + - textile +--- + +h2. Responding to the Client + +This is a comprehensive overview of the various ways in which to respond to +requests from clients, going from the simplest, standard response (using the +ok and not_found methods) to issuing standard errors +and even responding with the standard Rack format. + + +h2. Rack Response + +Since Halcyon is a Rack-based application, responding to requests follows the +simple format: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +[status, headers, body] +<% end -%> + +For example, a simple response could be: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +[200, {}, 'OK'] +<% end -%> + +In fact, the above is close to what results by calling ok. + + +h2. Standard Responses + +The ok method will be the primary response method call, simply +wrapping the body of the message into a standard Rack response with the status +set to 200 and headers set appropriately (though additional headers +are set before responses are sent) with the body being set with a specific +format: + +{:status => status, :body => body} + +The status code does appear twice: this is on purpose, providing the clients +with status information that they would otherwise have to repackage from the +HTTP response as well. Also, since JSON requires a minimum of a hash or array, +this meets this minimum requirement while allowing you to respond with simple +primitives such as plain integers or strings without having to wrap it manually. + +A simple example of using ok: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +ok "OK" #=> [200, {}, {:status => 200, :body => 'OK'}] +<% end -%> + +and in context: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def show + ok Message[params[:id]].to_json + end +end +<% end -%> + +This will take the instance of Message (a +"Sequel":http://code.google.com/p/ruby-sequel model object in this example) and +call to_json on it before passing it to ok which wraps +it in the standard Halcyon response format: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +[ + 200, + {}, + {:status => 200, :body => { + :id => 40, :message => 'foo'} + }.to_json +] +<% end -%> + +The above example shows you what the body of the response would be before +Halcyon converts it to JSON (hence to_json appearing at the end). + +The following methods are available: ok (status code: +200) and not_found (status code: 404). +More are planned for later. + + +h2. Errors + +Halcyon provides several exception classes that can be raised to simplify error +handling. These exceptions all model the standard HTTP errors, mapping to the +200, 300, 400, and 500 errors (and, in fact, they are not all errors, but can +still be raised to simplify responding with this status code and message). + +For example: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def create + msg = Message.create(params) + if msg.save + raise Created.new + else + ok UnprocessableEntity.new + end + end +end +<% end -%> + +It is not necessarily required or even recommended to raise an exception like +this to respond, but it is certainly possible and for a pure +"RESTful":http://wikipedia.org/wiki/RESTful application it can remove the tedium +of having to manually specify the status code and message. + +The most common example would be closer to the following: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def show + ok Message[params[:id]] or raise NotFound.new + end +end +<% end -%> + +The best place to see all of the status codes available would be to view the +source code ("view source":http://github.com/mtodd/halcyon/tree/master/lib/halcyon/exceptions.rb#L34-85) +or at http://www.askapache.com/htaccess/apache-status-code-headers-errordocument.html. + + +h2. Custom Responses + +As indicated above, the standard response format follows something like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +[status, headers, body] +<% end -%> + +This is the format indicated in the Rack specification for response. If you +would like to create a custom response, forgoing the standard ok +et al methods or the exception classes, or you do not need the body wrapped up +in a hash ( like {:status => status, :body => body}), you can +specify your own response manually. For instance: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def list + [200, {}, Message.all] + end +end +<% end -%> + +Extra flexibility with responses, such as just responding with the body and the +status being assumed 200, is planned for the future. diff --git a/content/docs/samples/weedb.txt b/content/docs/samples/weedb.txt new file mode 100644 index 0000000..3839b0a --- /dev/null +++ b/content/docs/samples/weedb.txt @@ -0,0 +1,17 @@ +--- +title: Docs — Samples — WeeDB +layout: simple +filter: + - erb + - textile +--- + +h2. WeeDB + +Source: "http://github.com/mtodd/halcyon/tree/master/examples/weedb/":http://github.com/mtodd/halcyon/tree/master/examples/weedb/ + +WeeDB is a clone of the "TinyDB":http://tinydb.org/ service. It is not +production quality code, but it is a good example of how to write a +database-driven Halcyon application. + +More coming soon. diff --git a/content/index.txt b/content/index.txt index 8ca49b7..b7b5914 100644 --- a/content/index.txt +++ b/content/index.txt @@ -17,6 +17,13 @@ Halcyon has several aims and goals, including: * *Be easy to implement* -- also easy since we're developing in "Ruby":http://ruby-lang.org/ here +h2. What Is Halcyon For? + +This is the question most often asked about Halcyon, what is Halcyon for? Simply put, Halcyon is a web application framework with a twist. The twist is simply that Halcyon applications communicate solely through "JSON":http://json.org/, both incoming and outgoing. + +This doesn't, however, answer what Halcyon is for. + + h2. Functionality & Performance With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. @@ -30,7 +37,7 @@ class Messages < Application end def create msg = Message.create(params) - msg.save # add error handling + msg.save ok msg.id end def read From 22172e0bf99847130b3330e7c8a05900eafe04aa Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 27 May 2008 20:00:02 -0400 Subject: [PATCH 14/27] Updated the Tutorial. --- content/docs/tutorial.txt | 156 ++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 58 deletions(-) diff --git a/content/docs/tutorial.txt b/content/docs/tutorial.txt index 6e36aa9..19d0806 100644 --- a/content/docs/tutorial.txt +++ b/content/docs/tutorial.txt @@ -5,71 +5,99 @@ filter: - erb - textile --- + h2. Tutorial -Coming to a new, unfamiliar framework can be daunting, especially with nobody there to hold your hand through the scary bits. Hopefully this tutorial will get you through those parts just fine and get you into developing cool services. +Coming to a new, unfamiliar framework can be daunting, especially with nobody +there to hold your hand through the scary bits. Hopefully this tutorial will +get you through those parts just fine and get you into developing cool +services. h3. Installation -If you've not installed Halcyon yet, read the "Installation":/docs/installation.html guide. +If you've not installed Halcyon yet, read the +"Installation":/docs/installation.html guide. h3. Starting a new application -If you're familiar with "Rails":http://rubyonrails.org/ or "Merb":http://merbivore.com/, you know that you can begin working on a new application very easily by issuing a simple command, similar to @rails app_name@. Halcyon provides a similar command to do the same. +If you're familiar with "Rails":http://rubyonrails.org/ or +"Merb":http://merbivore.com/, you know that you can begin working on a new +application very easily by issuing a simple command, similar to +@rails app_name@. Halcyon provides a similar command to do the same. -Run the following in your command line (make sure you're in a directory you're OK having your project created in): +Run the following in your command line (make sure you're in a directory you're +OK having your project created in):
    $ halcyon init app_name
    This will generate output similar to the following:
    
    -      create
    -      create  runner.ru
    -      create  README
    -      create  config
    -      create  config/initialize.rb
    -      create  config/config.yml
    -      create  app
    -      create  app/application.rb
    -      create  Rakefile
    -      create  lib
    -      create  lib/client.rb
    -      create  log
    +  create  
    +  create  app
    +  create  app/application.rb
    +  create  config
    +  create  config/config.yml
    +  create  config/init
    +  create  config/init/environment.rb
    +  create  config/init/hooks.rb
    +  create  config/init/requires.rb
    +  create  config/init/routes.rb
    +  create  lib
    +  create  lib/client.rb
    +  create  Rakefile
    +  create  README
    +  create  runner.ru
    +  create  log
     
    -This shows you what files were created, but more importantly, what files you'll be working with. +This shows you what files were created, but more importantly, what files you'll +be working with. Now, change into the @app_name@ directory:
    $ cd app_name
    -You are now the proud owner of a brand new Halcyon application. Now would be a good time to run @git init@ to begin tracking your app under Git's revision control. +You are now the proud owner of a brand new Halcyon application. Now would be a +good time to run @git init@ to begin tracking your app under Git's revision +control. + +*Note:* With halcyon init -g, the new application directory will +be initialized as a new Git repository. -G will go ahead and +commit the initial files. h3. Running Halcyon Apps -So with our brand new application, let's see what running our application looks like: +So with our brand new application, let's see what running our application looks +like:
    halcyon start -p 4647
    -This tells Halcyon to start up the Halcyon application using either Rack's @rackup@ utility or Thin's @thin start@ utility (if "Thin":http://code.macournoyer.com/thin/ is installed) along with the port to run the server on. +This tells Halcyon to start up the Halcyon application using either Rack's +@rackup@ utility or Thin's @thin start@ utility (if +"Thin":http://code.macournoyer.com/thin/ is installed) along with the port to +run the server on. You will see the following output:
    
     (Starting in /path/to/app_name)
    -DEBUG [2008-04-04 04:20:19] (11421) AppName :: Init: Initialize
    -DEBUG [2008-04-04 04:20:19] (11421) AppName :: Load: Application Controller
    - INFO [2008-04-04 04:20:19] (11421) AppName :: Starting up...
    - INFO [2008-04-04 04:20:19] (11421) AppName :: Initialize application resources and define routes in config/initialize.rb
    -DEBUG [2008-04-04 04:20:19] (11421) AppName :: Starting GC.
    - INFO [2008-04-04 04:20:19] (11421) AppName :: Started. PID is 11421
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Init: Requires
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Init: Hooks
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Init: Routes
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Init: Environment
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Load: Application Controller
    + INFO [2008-05-27 19:51:39] (9250) AppName :: Starting up...
    + INFO [2008-05-27 19:51:39] (9250) AppName :: Define startup tasks in config/init/hooks.rb
    +DEBUG [2008-05-27 19:51:39] (9250) AppName :: Starting GC.
    + INFO [2008-05-27 19:51:39] (9250) AppName :: Started. PID is 9250
     
    -This reveals a bit about its booting process and lets you know when it's ready to begin accepting connections. +This reveals a bit about its booting process and lets you know when it's ready +to begin accepting connections. In another shell window, keeping your Halcyon app running, run the following: @@ -78,15 +106,21 @@ $ irb -r rubygems -r halcyon >> client = Halcyon::Client.new('http://localhost:4647/') => # >> client.get('/time') -=> {"status"=>200, "body"=>"Fri Apr 04 04:24:15 -0400 2008"} +=> {"status"=>200, "body"=>"Tue May 27 19:53:15 -0500 2008"} >> exit <% end -%> -What this does is, after requiring the Halcyon library, we create an instance of @Halcyon::Client@, telling it where to connect to. +What this does is, after requiring the Halcyon library, we create an instance +of @Halcyon::Client@, telling it where to connect to. -After the client is created, we can then perform requests using standard HTTP request types, GET, POST, PUT, and DELETE. Here we simply call @get('/time')@ which gets routed to the @time@ action inside of the @Application@ controller inside of @app_name/app/application.rb@. +After the client is created, we can then perform requests using standard HTTP +request types, GET, POST, PUT, and DELETE. Here we simply call @get('/time')@ +which gets routed to the @time@ action inside of the @Application@ controller +inside of @app_name/app/application.rb@. -Don't worry, you'll be able to wrap up @get@ and @post@ requests in your own custom client methods and make corresponding actions in the actual Halcyon application. +Don't worry, you'll be able to wrap up @get@ and @post@ requests in your own +custom client methods and make corresponding actions in the actual Halcyon +application. h3. Modifying Your App @@ -94,7 +128,10 @@ h3. Modifying Your App h4. Controllers -Halcyon's controllers all inherit from Halcyon::Controller which provides several useful methods for responding in different situations, such as the @ok@ method to respond with the @200 OK@ standard HTTP success response, along with any data you need to send back. +Halcyon's controllers all inherit from Halcyon::Controller which provides +several useful methods for responding in different situations, such as the +@ok@ method to respond with the @200 OK@ standard HTTP success response, along +with any data you need to send back. For example, a controller may look like this: @@ -102,51 +139,46 @@ For example, a controller may look like this: class Messages < Application def new # respond with fields acceptable - ok [:body, :title, :tags] + ok Model.columns end def create - case method - when :post - DB[:messages] << params.merge(:tags => params[:tags].join) - ok - when :get - # the params required for new messages - ok [:message, :tags] - else - raise NotImplemented - end + msg = Message.create(params.merge(:tags => params[:tags].join)) + msg.save + ok msg.id end def read - ok DB[:messages][params[:id]] + ok Message[params[:id]] end def update - case method - when :post - DB[:messages].filter(:id => params[:id]).update(params) - ok - else - raise NotImplemented - end + Message.filter(:id => params[:id]).update(params) + ok end def delete - DB[:messages].filter(:id => params[:id]).delete + Message.filter(:id => params[:id]).delete ok end end <% end -%> -@DB@ refers to a "Sequel":http://code.google.com/p/ruby-sequel/ connection, which lets us talk to the @messages@ table. This could just as easily be a Sequel model, ActiveRecord model, or DataMapper model. +@Message@ refers to a "Sequel":http://code.google.com/p/ruby-sequel/ model, +which lets us talk to the @messages@ table. This could just as easily be a +Sequel model, ActiveRecord model, or DataMapper model. Read more about "Writing Controllers":/docs/controllers.html h4. Routes -Part of developing a Halcyon app is writing the controllers, but requests need to be routed to the appropriate actions. +Part of developing a Halcyon app is writing the controllers, but requests need +to be routed to the appropriate actions. -There are, by default, no routes defined for an application, but there is a way to quickly define routes as matching any variation of @/:controller/:action/:id@, etc. Here is a sample, including a custom route as well: +There are, by default, no routes defined for an application, but there is a way +to quickly define routes as matching any variation of +@/:controller/:action/:id@, etc. Here is a sample, including a custom route as +well: <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## /path/to/app_name/config/init/routes.rb Halcyon::Application.route do |r| r.match('/api/:version/:controller/:action(/:id)?').to() r.default_routes @@ -158,7 +190,10 @@ Read more about "Defining Routes":/docs/routes.html. h4. Clients -The easiest way to communicate with your Halcyon application is with a Halcyon client. By default, it creates a simple way to perform GET, POST, PUT, and DELETE requests on application routes, but can be extended with methods that easily corresponds with your routes. For example: +The easiest way to communicate with your Halcyon application is with a Halcyon +client. By default, it creates a simple way to perform GET, POST, PUT, and +DELETE requests on application routes, but can be extended with methods that +easily corresponds with your routes. For example: <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> >> class MessageClient < Halcyon::Client @@ -181,6 +216,11 @@ Read more about "Customizing Clients":/docs/clients.html h2. What's Next -Now that you know how to get things running, you'll want to delve deeper into learning just how to customize your application by reading "Defining Routes":/docs/routes.html, "Writing Controllers":/docs/controllers.html, and "Customizing Clients":/docs/clients.html. +Now that you know how to get things running, you'll want to delve deeper into +learning just how to customize your application by reading +"Defining Routes":/docs/routes.html, +"Writing Controllers":/docs/controllers.html, and +"Customizing Clients":/docs/clients.html. -Still confused? Read a more thorough "Introduction to Halcyon":/docs/introduction.html. +Still confused? Read a more thorough +"Introduction to Halcyon":/docs/introduction.html. From 87aa585a09b5a00b2bfdd19bcdf7d643a46db961 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 29 May 2008 16:08:32 -0400 Subject: [PATCH 15/27] Added brief text about what Halcyon is for on the home template. --- layouts/default.rhtml | 1 + 1 file changed, 1 insertion(+) diff --git a/layouts/default.rhtml b/layouts/default.rhtml index 10951e7..7be6049 100644 --- a/layouts/default.rhtml +++ b/layouts/default.rhtml @@ -50,6 +50,7 @@ filter: .box %h2 About %p Halcyon is a JSON Web App Framework built on Rack for speed and light weight. + %p It is ideal for creating light-weight service application layers, such as APIs for existing apps, etc. %h2 Recent Entries #twitter_div From 682f066aafd549ebce9153b563b0fc123f72b5a7 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 3 Jun 2008 19:03:07 -0400 Subject: [PATCH 16/27] Clarified what Halcyon is for on the main page. --- content/index.txt | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/content/index.txt b/content/index.txt index b7b5914..fca4e01 100644 --- a/content/index.txt +++ b/content/index.txt @@ -16,17 +16,36 @@ Halcyon has several aims and goals, including: * *Be flexible* -- since it uses HTTP, it's very simple to be flexible * *Be easy to implement* -- also easy since we're developing in "Ruby":http://ruby-lang.org/ here +Simply put, Halcyon is a web application framework with a +twist. The twist is simply that Halcyon applications communicate +solely through "JSON":http://json.org/, both incoming and outgoing. + h2. What Is Halcyon For? -This is the question most often asked about Halcyon, what is Halcyon for? Simply put, Halcyon is a web application framework with a twist. The twist is simply that Halcyon applications communicate solely through "JSON":http://json.org/, both incoming and outgoing. +This is the question most often asked about Halcyon, what is Halcyon +for? If "Rails":http://rubyonrails.org/ (and "Merb":http://merbivore.com/ +"et al":http://ramaze.net/#other-frameworks) is for quickly developing web +applications, Halcyon aims to provide a framework for developing +service-oriented applications (SOAs) such as APIs or other non-interfaced +services. + +The "Twitter":http://twitter.com/ API is a great example of a SOA where +tweets can be submitted without needing any web interface. The power of this +type of application is that other client-side applications can be developed to +provide an interface to the web service. + +Halcyon aims to make writing these types of application interfaces and other +similar services trivial. -This doesn't, however, answer what Halcyon is for. h2. Functionality & Performance -With Mongrel leading the pack and Rack holding things up, JSON doing the fast talking and with plenty of room to spare, how could you not be interested, even about this new framework? Still not convinced? OK, fair enough, here's some code for you. +With Mongrel leading the pack and Rack holding things up, JSON doing the fast +talking and with plenty of room to spare, how could you not be interested, even +about this new framework? Still not convinced? OK, fair enough, here's some +code for you. <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> class Message < Sequel::Model; end @@ -58,21 +77,29 @@ You can then run it with:
    $ halcyon start -p 4647
    -That's all it takes to open up the door to let you communicate with your applications that implement or use the simple client. +That's all it takes to open up the door to let you communicate with your +applications that implement or use the simple client. Read the "Docs":/docs/. h2. Supported Platforms -Halcyon is primarily written in Ruby, but Halcyon also supports multiple platforms due to the fact that it communicates via HTTP and packages its messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, with more clients planned. +Halcyon is primarily written in Ruby, but Halcyon also supports multiple +platforms due to the fact that it communicates via HTTP and packages its +messages in JSON. Halcyon currently has Ruby, PHP, and Java clients available, +with more clients planned. -You can see the various client implementations at http://github.com/mtodd/halcyon-clients (except for the Ruby client, which is part of the Halcyon gem). +You can see the various client implementations at +"the GitHub project":http://github.com/mtodd/halcyon-clients (except for the +Ruby client, which is part of the Halcyon gem). h2. Metrics -Ohloh's pretty cool and we use it to track the development metrics of Halcyon. Check out some of the more interesting details on our project page. You can find the link at the top of the page. +Ohloh's pretty cool and we use it to track the development metrics of Halcyon. +Check out some of the more interesting details on our project page. You can +find the link at the top of the page. For your viewing pleasure, here are some of the Ohloh project factoids: From d918845332b73e6cd97c5a86676b3f72391f923d Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 10 Jun 2008 20:15:36 -0400 Subject: [PATCH 17/27] Added blurb about the usefulness of Halcyon. --- content/docs/introduction.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/content/docs/introduction.html b/content/docs/introduction.html index 8d1674d..da76d05 100644 --- a/content/docs/introduction.html +++ b/content/docs/introduction.html @@ -21,6 +21,11 @@ HTTP/JSON requests, removing the need for content-negotiation or explicitly formatting everything to the JSON format. +On top of this technical level, the goal of Halcyon is to make building +service-oriented applications quick and easy, removing the general cruft of +rendering JSON, etc. Halcyon tries to handle all of the mechanics +associated with getting an SOA running. + h3. An Example From a898965e82fc90199778864f9ff99a0008e1b4f7 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Tue, 10 Jun 2008 23:56:20 -0400 Subject: [PATCH 18/27] Reorganized the documentation links. --- content/docs/index.txt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/content/docs/index.txt b/content/docs/index.txt index dee6f0c..3051031 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -19,13 +19,17 @@ h3. Getting Started * "Tutorial":/docs/tutorial.html -h3. Advanced Topics +h3. The Basics -* "Defining Routes":/docs/routes.html * -"Writing Controllers":/docs/controllers.html- +* "Defining Routes":/docs/routes.html +* "Responding to the Client":/docs/responding.html + + +h3. Advanced Topics + * "Connecting to Databases":/docs/databases.html * -"Customizing Clients":/docs/clients.html- -* "Responding to the Client":/docs/responding.html * "Advanced Rack Topics":/docs/rack.html * -"Troubleshooting":/docs/troubleshooting.html- From d1bf18df76edb9b6e3213ca8cd7eb84153a6953b Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Wed, 11 Jun 2008 06:02:06 -0400 Subject: [PATCH 19/27] Added initial documentation for writing controllers. --- content/docs/controllers.html | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/content/docs/controllers.html b/content/docs/controllers.html index c8208e2..f3ca456 100644 --- a/content/docs/controllers.html +++ b/content/docs/controllers.html @@ -5,6 +5,65 @@ - erb - textile --- + h2. Writing Controllers -Coming soon. +Controllers are the functional heart of your application. Requests to your +application get routed to controllers where the actions defined within them are +dispatched. + +Those of you familiar with "Rails":http://rubyonrails.org/ or "MVC":http://wikipedia.org/wiki/Model-view-controller +in general will recognize the role that the controller and its actions play. +While the models will contain the primary portion of logic, controllers will +coordinate it all. + +Really, there's nothing special about Halcyon controllers. Let's look at what +they look like. + + +h3. Controller Structure + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + + def show + ok Message[params[:id]] + end + +end +<% end -%> + +The @Application@ class we inherit from simply from @Halcyon::Controller@ and +provides a place for utility methods et al. @Application@ is created by default +when generating an application. + + +h3. Actions + +Actions make up the functional portion of classes. Actions are considered any +public method (private methods are not callable through routes). + +Actions have several useful methods, two of which will be used in most actions: +@params@ and @ok@. The @params@ method provides access to the parameters +available, such as GET params, POST params, and route params. @ok@ is used to +format responses and is akin to calling @render :json => val@ in Rails. + + +h3. Resources + +The "REST":http://wikipedia.org/wiki/REST approach to application design treats +our models as resources with a standard set of methods to work them them. +Again, if you're familiar with Rails development, none of this is new. Here is +an example controller defining these standard REST methods. + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + + def show + ok Message[params[:id]] + end + +end +<% end -%> + +... From 203220344b7c8be433128b9f9c008ee1f0192ef5 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 10:07:50 -0400 Subject: [PATCH 20/27] Updated documentation for writing controllers and added list of Exceptions. --- content/docs/controllers.html | 66 +++++++++++++++++++++++- content/docs/exceptions.html | 94 +++++++++++++++++++++++++++++++++++ content/docs/index.txt | 1 + 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 content/docs/exceptions.html diff --git a/content/docs/controllers.html b/content/docs/controllers.html index f3ca456..f5f4ac0 100644 --- a/content/docs/controllers.html +++ b/content/docs/controllers.html @@ -63,7 +63,71 @@ ok Message[params[:id]] end + def create + ok Message << params + end + + def update + Message.filter(:id => params[:id]).update(params) + ok + end + + def delete + Message.filter(:id => params[:id]).delete + ok + end + +end +<% end -%> + +Resources are mapped to these methods through routing method @resource@. This +is one of the benefits of using the "Merb":http://merbivore.com/ router. + +Read more about "writing routes":/docs/routes.html for great coverage of this +topic. + + +h3. Error Handling and Exceptions + +There will inevitably be errors that need to be handled and exceptions are a +big part of gracefully working with errors in a meaningful way. Halcyon +provides all of the standard HTTP responses as exceptions to help with quickly +communicating the appropriate status of a request, and Halcyon handles +exceptions to gracefully communicate with the client the appropriate status in +the standard format. + +Here is an example of handling success or failure: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + + def show + if (msg = Message[params[:id]]) + ok msg + else + raise NotFound.new + end + end + + def create + msg = Message.new + msg.values.merge! params + if (id = msg.save) + ok id + else + raise UnprocessableEntity.new + end + end + end <% end -%> -... +The @UnprocessableEntity@ exception class maps directly to the standard HTTP +response code @422 Unprocessable Entity@ which signifies that there were errors +creating the record as the models validations failed. You can certainly supply +the exception with a body other than the literal text @"Unprocessable Entity"@ +which could be the exact error (which is recommended). This is up to you, of +course. + +Check out the "list of exceptions":/docs/exceptions.html to see what's +available and how to use them. diff --git a/content/docs/exceptions.html b/content/docs/exceptions.html new file mode 100644 index 0000000..240ed1a --- /dev/null +++ b/content/docs/exceptions.html @@ -0,0 +1,94 @@ +--- +title: Docs — Exceptions +layout: simple +filter: + - erb + - textile +--- + +h2. Exceptions + +The most up-to-date list of HTTP status codes that Halcyon supports can be seen +at the "GitHub source page":http://github.com/mtodd/halcyon/tree/master/lib/halcyon/exceptions.rb +which is transformed into actual exception classes from the error message. + +Here is a list of the actual Exception classes as created by this list and a +brief overview of when to use these exceptions: + +* @100 Continue@ +* @101 SwitchingProtocols@ +* @102 Processing@ +* @200 OK@ — this is the standard response, alias method @ok@ +* @201 Created@ — can be used to indicated success for @create@ action +* @202 Accepted@ +* @203 NonAuthoritativeInformation@ +* @204 NoContent@ +* @205 ResetContent@ +* @206 PartialContent@ +* @207 MultiStatus@ +* @300 MultipleChoices@ +* @301 MovedPermanently@ +* @302 MovedTemporarily@ +* @303 SeeOther@ +* @304 NotModified@ +* @305 UseProxy@ +* @307 TemporaryRedirect@ +* @400 BadRequest@ — can be used to indicate insufficient params, etc +* @401 Unauthorized@ — can indicate authorization necessary +* @402 PaymentRequired@ +* @403 Forbidden@ +* @404 NotFound@ — can indicate resources not found +* @405 MethodNotAllowed@ +* @406 NotAcceptable@ — can be used to indicate method is restricted to certain clients et al +* @407 ProxyAuthenticationRequired@ +* @408 RequestTimeout@ +* @409 Conflict@ +* @410 Gone@ +* @411 LengthRequired@ +* @412 PreconditionFailed@ +* @413 RequestEntityTooLarge@ +* @414 RequestURITooLarge@ +* @415 UnsupportedMediaType@ +* @416 RequestedRangeNotSatisfiable@ +* @417 ExpectationFailed@ +* @422 UnprocessableEntity@ — can be used to indicate models fail validations +* @423 Locked@ +* @424 FailedDependency@ +* @425 NoCode@ +* @426 UpgradeRequired@ — can be used to indicate change of API versions, etc +* @500 InternalServerError@ — indicates uncaught error +* @501 NotImplemented@ — can indicate non-functionality or partial implementation +* @502 BadGateway@ +* @503 ServiceUnavailable@ — can indicate the service is temporarily unavailable for maintenance +* @504 GatewayTimeout@ +* @505 HTTPVersionnotsupported@ +* @506 VariantAlsoNegotiates@ +* @507 InsufficientStorage@ +* @510 NotExtended@ + + +h3. Making Your Own + +Although the standard exceptions available should be sufficient (they are +the HTTP response codes), you could certainly make up your own HTTP response +codes. Putting this inside of an @init@ file or in the @lib@ directory should +be fine: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +## lib/custom_exception.rb +## or config/init/custom_exception.rb +class CustomException < Halcyon::Exceptions::Base + + def initialize(body = 'Custom Exception') + super(460, body) + end + +end +<% end -%> + +As you can see, the status code is the first argument to the base @initialize@ +method and the @body@ variable is set to the default error text. Obviously, the +exception name, file name, and error status should be more descriptive. +Pick a sane error code as well, something that hasn't been used officially. + +If at all possible, use the default response codes/exceptions. diff --git a/content/docs/index.txt b/content/docs/index.txt index 3051031..f8c37d6 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -31,6 +31,7 @@ h3. Advanced Topics * "Connecting to Databases":/docs/databases.html * -"Customizing Clients":/docs/clients.html- * "Advanced Rack Topics":/docs/rack.html +* "Exceptions":/docs/exceptions.html * -"Troubleshooting":/docs/troubleshooting.html- From 287941dcad8176e2714d9132fae1ab5d4f45b8ab Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 11:09:19 -0400 Subject: [PATCH 21/27] Updated the routes documentation and remove the strikeout for the controllers article. --- content/docs/index.txt | 2 +- content/docs/routes.html | 135 ++++++++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 32 deletions(-) diff --git a/content/docs/index.txt b/content/docs/index.txt index f8c37d6..e2e2108 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -21,7 +21,7 @@ h3. Getting Started h3. The Basics -* -"Writing Controllers":/docs/controllers.html- +* "Writing Controllers":/docs/controllers.html * "Defining Routes":/docs/routes.html * "Responding to the Client":/docs/responding.html diff --git a/content/docs/routes.html b/content/docs/routes.html index df636b0..2707b1c 100644 --- a/content/docs/routes.html +++ b/content/docs/routes.html @@ -5,44 +5,63 @@ - erb - textile --- + h2. Defining Routes -One of the most peculiar parts of Halcyon is its dependency on "Merb":http://merbivore.com/, but this is for a very good reason: Merb provides a great deal of great code that is modular and clean, perfect to implement into Halcyon. This has two affects: first, those pieces of code are very well documented by a very large and active community, and secondly is that they are continually being updated to better perform. Rewriting what Merb has already done would be silly. *So when it comes to defining routes in Halcyon, much of the documentation for defining routes in Merb still applies!* +One of the most peculiar parts of Halcyon is its dependency on +"Merb":http://merbivore.com/, but this is for a very good reason: Merb provides +a great deal of great code that is modular and clean, perfect to implement into +Halcyon. This has two affects: first, those pieces of code are very well +documented by a very large and active community, and secondly is that they are +continually being updated to better perform. Rewriting what Merb has already +done would be silly. *So when it comes to defining routes in Halcyon, much of +the documentation for defining routes in Merb still applies!* -For links to various Routing documentation for Merb, jump to the bottom of the page and look under the Resources section. +For links to various Routing documentation for Merb, jump to the bottom of the +page and look under the Resources section. h3. Getting Started -Routes are defined in @app_name/config/initialize.rb@, wherein you will find something like this by default: +Routes are defined in @app_name/config/init/routes.rb@, wherein you will find +something like this by default: <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> -# = Required Libraries -%w().each {|dep|require dep} - -# = Initialization -class Halcyon::Application - - startup do |config| - self.logger.info 'Initialize application resources and define routes in config/initialize.rb' - end - - # = Routes - route do |r| - r.match('/time').to(:controller => 'application', :action => 'time') - - r.match('/').to(:controller => 'application', :action => 'index') - - # failover - {:action => 'not_found'} - end - +# = Routes +Halcyon::Application.route do |r| + + # Sample route for the sample functionality in Application. + # Safe to remove! + r.match('/time').to(:controller => 'application', :action => 'time') + + # RESTful routes + # r.resources :posts + + # This is the default route for /:controller/:action/:id + # This is fine for most cases. If you're heavily using resource-based + # routes, you may want to comment/remove this line to prevent + # clients from calling your create or destroy actions with a GET + r.default_routes + + # Change this for the default route to be available at / + r.match('/').to(:controller => 'application', :action => 'index') + # It can often be useful to respond with available functionality if the + # application is a public-facing service. + + # Default not-found route + {:action => 'not_found'} + end <% end -%> -In the lower half you see where two routes are defined and one failover route is specified. (This failover route is actually set by default, but it is provided here as well to indicate how to update this default easily). +In the lower half you see where two routes are defined and one failover route +is specified. (This failover route is actually set by default, but it is +provided here as well to indicate how to update this default easily). -Within the @route@ block, @r@ is used to define what routes to match against and where to route those requests to. Routes can be very specific or very general, accepting no or many variables in the route itself. Here are several examples to hopefully clarify the flexibility of these routes: +Within the @route@ block, @r@ is used to define what routes to match against +and where to route those requests to. Routes can be very specific or very +general, accepting no or many variables in the route itself. Here are several +examples to hopefully clarify the flexibility of these routes: <% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> r.match('/api/:version/app_name/:controller/:action').to() @@ -52,18 +71,72 @@ r.match('/').to(:controller => 'application', :action => 'usage') <% end -%> -The @default_routes@ method is also one provided for by Merb and can also provide extra functionality as well as clarifies some other useful methods like @defer_to@ for conditional routes. Read below in the Resources section for more information. - -_More coming soon!_ +The @default_routes@ method is also one provided for by Merb and can also +provide extra functionality as well as clarifies some other useful methods like +@defer_to@ for conditional routes. Read below in the Links section for more +information. h3. Resources -Merb's API provides two very useful resources for defining routes. These two are the methods used to match paths and define how to handle those routes. These links are: +One of the more power routing mechanics is the definition of resources which +map to "REST":http://wikipedia.org/wiki/REST functionality through standard +actions (discussed in the "writing controllers":/docs/controllers.html +article). + +Defining resources' routes is trivial and the routes that are defined doing so +should cover most uses of a given resource (with the ability to define extended +functionality with the rest of the Merb routes API). Here's an example of +defining a resource route: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +Halcyon::Application.route do |r| + + r.resources :messages + +end +<% end -%> + +Here are the routes that get generated with this: + +
    +GET /messages
    +POST /messages
    +GET /messages/:id
    +PUT /messages/:id
    +DELETE /messages/:id
    +
    + +These effectively map to the the @list@, @create@, @show@, @update@, and +@delete@ actions in the @Messages@ controller, respectively. It's important to +note that the controller it expects is the camel case of the resource. It's +also an acceptable (and possibly even recommended) practice to name your model +the singular form, thereby having the @Message@ resource mapped to the +@Messages@ resource controller. This provides a very sane mental mapping when +working with the code. + + +h3. Links + +Merb's API provides two very useful resources for defining routes. These two +are the methods used to match paths and define how to handle those routes. +These links are: * "Merb::Router::Behavior#match":http://merbivore.com/documentation/merb-core/head/index.html?a=M000787&name=match * "Merb::Router::Behavior#to":http://merbivore.com/documentation/merb-core/head/index.html?a=M000790&name=to -The @Merb::Router::Behavior@ class is used to generate each route, which you will recognize it as the block parameter passed and used similar to @r.match('/').to(:controller => 'application', :action => 'index')@. +The @Merb::Router::Behavior@ class is used to generate each route, which you +will recognize it as the block parameter passed and used similar to +@r.match('/').to(:controller => 'application', :action => 'index')@. + +Also, the "Merb::Router::Behavior#default_routes":http://merbivore.com/documentation/merb-core/head/index.html?a=M000792&name=default_routes +method may be worth investigating as it handles defining common routes like +@/:controller/:action/:id@ and the like. + +Merb provides documentation for the @resources@ route definition as well at +"Merb::Router::Behavior#resources":http://merbivore.com/documentation/merb-core/head/index.html?a=M000998&name=resources. + +Check out these other great links as well: -Also, the "Merb::Router::Behavior#default_routes":http://merbivore.com/documentation/merb-core/head/index.html?a=M000792&name=default_routes method may be worth investigating as it handles defining common routes like @/:controller/:action/:id@ and the like. +* "An Introduction to Routing":http://merbunity.com/tutorials/12 at "Merbunity":http://merbunity.com/ +* "Routing":http://wiki.merbivore.com/pages/routing at the "Merb Wiki":http://wiki.merbivore.com/ From 3638fed4b39512653d123c327d6d0232d4cd0acd Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 15:41:02 -0400 Subject: [PATCH 22/27] Added Configuration and Logging documentation and updated Clients and Databases (minimally). --- content/docs/clients.html | 1 + content/docs/configuration.html | 189 ++++++++++++++++++++++++++++++++ content/docs/databases.txt | 1 + content/docs/index.txt | 2 + content/docs/logging.html | 74 +++++++++++++ 5 files changed, 267 insertions(+) create mode 100644 content/docs/configuration.html create mode 100644 content/docs/logging.html diff --git a/content/docs/clients.html b/content/docs/clients.html index 3d6cda5..9d43b63 100644 --- a/content/docs/clients.html +++ b/content/docs/clients.html @@ -5,6 +5,7 @@ - erb - textile --- + h2. Customizing Clients Coming soon. diff --git a/content/docs/configuration.html b/content/docs/configuration.html new file mode 100644 index 0000000..8533c43 --- /dev/null +++ b/content/docs/configuration.html @@ -0,0 +1,189 @@ +--- +title: Docs — Configuration +layout: simple +filter: + - erb + - textile +--- + +h2. Configuration + +There are a great deal of configuration points in your application, including +but not limited to the @config/config.yml@ settings file. + + +h3. @config/config.yml@ + +This file often starts like this: + +<% coderay(:lang => "yaml", :line_numbers => "inline", :tab_width => 2) do -%> +--- +## config/config.yml +# = Framework +# +allow_from: all + +# = Environment +# +# environment: production + +# = Logging +# +logging: + type: Logger + # file: # STDOUT + level: debug + +# = Application +# +# Your application-specific configuration options here. +<% end -%> + +(Extra comments have been removed for the sake of brevity.) + +In this file you can specify whether your application allows requests to come +from clients locally (only requests from @localhost@), from Halcyon clients +(ignoring any non-Halcyon client), or all (which is the default). + +You can also explicitly specify which environment to run under and how to log +messages, including where to save the logged messages and what level to save. +Read more about "configuring logging":/docs/logging.html. + + +h3. Boot/Initialization + +The boot process also provides a great way to customize your application, +including adding in dependencies and wiring in new functionality. +Initialization is handled by the files in @config/init/@ such as @requires.rb@ +and @routes.rb@ etc. + +Let's take a look at each file. + + +h4. Requires + +The file @config/init/requires.rb@ includes any necessary dependencies for your +application, including your preferred ORM library and anything else necessary +to your application's operation. + +By default, the requires init file is empty, requiring nothing. This is what it +should look like for a freshly created app: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +%w().each{|dep|require dep} +<% end -%> + +If you're not familiar with the syntax used here, this is simply another way to +define an array of strings and require each entry as its own dependency. The +following code snippets are identical and are both valid code for the +@requires.rb@ file: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +%w(sequel drb).each{|dep|require dep} + +## OR + +require 'sequel' +require 'drb' +<% end -%> + +Initially it may seem like more code, but for longer lists of dependencies, it +can save a lot of repetition. + + +h4. Environment + +The file @config/init/environment.rb@ shouldn't require a great deal of change +since it simply wires @Halcyon.environment@ to the +@Halcyon.config[:environment]@ configuration value and sets the default value +if not set already. + +However, feel free to alter this and any file at will (so long as you know what +you're doing, and sometimes even when you don't). + + +h4. Routes + +The file @config/init/routes.rb@ contains the definition of the routes. There +is an in-depth article going over "routes":/docs/routes.html with plenty of +links to further documentation. + + +h4. Hooks + +The file @config/init/hooks.rb@ contains the definition of the startup, +shutdown, and any other hooks available. This allows you to run some setup or +shutdown tasks to be run after the configuration and all other dependencies +have been loaded. This is ideal for connecting to databases or opening other +resources necessary for the operation of your application. + +You can see where in the boot process the hooks are run by starting a brand new +Halcyon application and then shutting it down. Look for notifications for where +to define startup and shutdown hooks, this is when the code is run. + + +h4. @config/init/*.rb@ + +Other files in the @init@ folder are also run during boot so you can put any +Ruby file in there and it will be run at boot. For example, a @database.rb@ +file is certainly appropriate to setup database configuration values, etc. + + +h2. Rack and @runner.ru@ + +The last point of customization exists between the application itself and the +server running it through "Rack":http://rack.rubyforge.org/. Since Halcyon is a +simple Rack application and Rack applications can be layered, it's perfectly +acceptable to layer in static file serving (for development only, stick to +something faster like Nginx or Apache for production) or for handling file +uploads or other really-long-running processes (until we wire in the deferrable +actions which spawn off as their own threads where necessary like Merb). + +There are also several standard Rack middleware available such as +"Cascade":http://rack.rubyforge.org/doc/classes/Rack/Cascade.html which finds +the first application in an array of applications (such as a Halcyon app +followed by a Rails app) to return a non-404 response, or the "Reloader":http://rack.rubyforge.org/doc/classes/Rack/Reloader.html +middleware which reloads changed classes if changed between requests. There are +still more interesting middleware available. + +Here's a sample @runner.ru@ file used by one of the example applications +distributed with Halcyon's source: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +require 'halcyon' + +$:.unshift(Halcyon.root/'lib') +puts "(Starting in #{Halcyon.root})" +Thin::Logging.silent = true if defined? Thin + +# = Apps +# The applications to try. +apps = [] + +# = Redirecter +# Requests to / get redirected to /index.html. +apps << lambda do |env| + case env['PATH_INFO'] + when '/' + puts " ~ Redirecting to /index.html" + [302, {'Location' => '/index.html'}, ""] + else + [404, {}, ""] + end +end + +# = Static Server +# Make sure that the static resources are accessible from the same address so +# we don't have to worry about the Same Origin stuff. +apps << Rack::File.new(Halcyon.root/'static') + +# = Halcyon App +apps << Halcyon::Runner.new + +# = Server +# Run the Cascading server +run Rack::Cascade.new(apps) +<% end -%> + +This will serve static files necessary for running the application, passing +through non-matches to the actual Halcyon application. diff --git a/content/docs/databases.txt b/content/docs/databases.txt index ef2a8d0..66b1c68 100644 --- a/content/docs/databases.txt +++ b/content/docs/databases.txt @@ -5,6 +5,7 @@ filter: - erb - textile --- + h2. Connecting to Databases Although Halcyon doesn't come with any ORM-specific plumbing, getting connected diff --git a/content/docs/index.txt b/content/docs/index.txt index e2e2108..3d4c100 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -24,11 +24,13 @@ h3. The Basics * "Writing Controllers":/docs/controllers.html * "Defining Routes":/docs/routes.html * "Responding to the Client":/docs/responding.html +* "Configuration":/docs/configuration.html h3. Advanced Topics * "Connecting to Databases":/docs/databases.html +* "Logging":/docs/logging.html * -"Customizing Clients":/docs/clients.html- * "Advanced Rack Topics":/docs/rack.html * "Exceptions":/docs/exceptions.html diff --git a/content/docs/logging.html b/content/docs/logging.html new file mode 100644 index 0000000..c176e30 --- /dev/null +++ b/content/docs/logging.html @@ -0,0 +1,74 @@ +--- +title: Docs — Logging +layout: simple +filter: + - erb + - textile +--- + +h2. Logging + +When troubleshooting your application or monitoring its activity, logging +provides a central source of statistics and a record of events. In Halcyon, +several types of loggers are support, including the Ruby standard @Logger@, but +also "Analogger":http://analogger.swiftcore.org/, Logging, and Log4r. + + +h3. Configuration + +The default configuration for logging is to just output straight to standard +out, including all debugging information. However, this can be adjusted +according to your needs (such as in production). + +The logging configuration settings are found in @config/config.yml@ under the +@logging@ heading. Depending on which logger client specified, the options +available can change, but by default you have @file@ and @level@. + +If @file@ is @nil@ (commented out or left blank), standard output is used +instead of logging to a file. However, if you would like to log to a file, it +is suggested to log to log/environment.log (where +environment is which environment you're running in, @development@, +@test@, or @production@). This can be anything at all, though, including +@/var/log/app_name.log@ (as long as the application is being run with the right +permissions or access levels). + +For example, to only log messages of type @WARN@ with the standard Ruby logger +to the local production log, the following configuration options would suffice: + +<% coderay(:lang => "yaml", :line_numbers => "inline", :tab_width => 2) do -%> +--- +## config/config.yml +logging: + type: Logger + file: log/production.log + level: warn +<% end -%> + + +h3. Logging Messages + +If you're needing log messages, every object is extended by the Logging helper +and provides a method called @logger@. In most circumstances, you can call the +logger like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + def create + msg = Message.new + msg.values.merge! params + if id = msg.save + self.logger.debug "Created Message with #{msg.values.inspect}" + ok id + else + msg.errors.each do |error| + self.logger.warn "Unable to save model: " << error + end + raise UnprocessableEntity.new + end + end +end +<% end -%> + +The essence is @self.logger.level@ where level is an +acceptable logging level, including @debug@, @info@, @warn@, @error@, and +@fatal@ for the default Ruby logger. From 3b657679a0b5f28e4f1db01823cb7408689096ab Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 18:43:37 -0400 Subject: [PATCH 23/27] Documented clients. --- content/docs/clients.html | 205 +++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/content/docs/clients.html b/content/docs/clients.html index 9d43b63..de0a18d 100644 --- a/content/docs/clients.html +++ b/content/docs/clients.html @@ -8,4 +8,207 @@ h2. Customizing Clients -Coming soon. +*Note*: This article concerns the Ruby client for your application specifically +but most of the principles should still be applicable for clients not written +in Ruby. + +Depending on how you plan to deploy your Halcyon application, either it will be +accessed via any number of clients (@curl@ et al) or you can provide a +customized client interface for your app (or both, really). A great deal of +this process involves designing a good interface to your application via a +remote client, but looking past that, let's take a look at the technical +aspects of customizing a client for your application. + + +h3. The Application + +Let's start with a simple application whose controller looks like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +class Messages < Application + + def list + ok Message.limit(10).all + end + + def show + ok Message[params[:id]] + end + + def create + ok Message << params + end + + def update + Message.filter(:id => params[:id]).update(params) + ok + end + + def delete + Message.filter(:id => params[:id]).delete + ok + end + +end +<% end -%> + +Though this is a simplistic approach (in production we would want and need much +more in terms of handling errors) it should suffice. + +This application manages a single @Message@ resource which we'll assume +consists of nothing other than a text message of a certain size (say, 140 +characters, similar to "Twitter":http://twitter.com/). The routes are defined +like this: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +Halcyon::Application.route do |r| + + r.resources :messages + +end +<% end -%> + +This means that we will primarily interact with the application with the +following routes: + +
    +GET /messages
    +POST /messages
    +GET /messages/:id
    +PUT /messages/:id
    +DELETE /messages/:id
    +
    + +In the future we may want to associate users with messages, but for now we'll +just clump them all together in a single faceless cloud. + +The model itself is simple enough: it just provides a mapping for the database, +but we will not define it here (though we are using "Sequel":http://code.google.com/p/ruby-sequel/ +syntax for performing actions on the model). + +For our purposes, our application will be called @Messanger@. + + +h3. The Client + +On the client side we may want to define a pseudo model to behave functionally +like the actual @Message@ model on the server side, but we'll leave that as an +exercise for the reader; for now we'll just focus on defining the messaging +client to be able to submit requests and handle responses from the server. + +Let's go ahead and look at what our message client will look like: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +module Messanger + + class Client < Halcyon::Client + + # get list of messages + def list + if (msgs = get('/messages'))[:status] == 200 + # success + msgs[:body] # return message + else + # failure + msgs # return status and error message + end + end + + # get a single message + def show(id) + if (msg = get('/messages/'+id))[:status] == 200 + # success + msg[:body] # return message + else + # failure + msg # return status and error message + end + end + + # create a message + def create(message) + if (msg = post('/messages', :message => message))[:status] == 200 + # success + return msg[:body] # the new message id + else + # failure + return msg + end + end + + # update a message + def update(id, message) + if (msg = put('/messages/'+id, :message => message))[:status] == 200 + # success + return true + else + # failure + return msg + end + end + + # delete a message + def delete(id) + if (msg = delete('/messages/'+id))[:status] == 200 + # success + return true + else + # failure + return msg + end + end + + end + +end +<% end -%> + +Not the best code in the world and pretty repetitive. These are certainly +things that can be improved upon (and should be) with abstraction methods and +possibly even enabling exceptions (where exceptions are raised if a non-200 +response is given). + +Also, if we chose to use more descriptive HTTP response codes, such as @201 +Created@ instead of just @200 OK@ for the @create@ method, we could change our +code to better take advantage of this descriptive consistency related to the +"REST":http://wikipedia.org/wiki/REST approach. This is highly recommended. + +Let's take a look at actually using this client in IRB. We'll assume we're also +running the @Messanger@ application on port @4647@ (a common port for Halcyon +apps). + +
    +$ irb -r lib/client
    +>> client = Messanger::Client.new('http://localhost:4647/')
    +=> #
    +>> client.list
    +=> []
    +>> client.show(12)
    +=> {:status=>404, :body=>'Not Found'}
    +>> client.create('Hi!')
    +=> 1
    +>> client.list
    +=> [{:id=>1, :message=>'Hi!'}]
    +>> client.create('Howdy!')
    +=> 2
    +>> client.list
    +=> [{:id=>1, :message=>'Hi!'}, {:id=>2, :message=>'Howdy!'}]
    +>> client.show(1)
    +=> {:id=>1, :message=>'Hi!'}
    +>> client.update(1, 'Bamboozle...')
    +=> true
    +>> client.get('/messages/1')[:body]
    +=> {:id=>1, :message=>'Bamboozle...'}
    +>> client.delete(2)
    +=> true
    +>> client.delete(2)
    +=> {:status=>404, :body=>'Not Found'}
    +>> client.list
    +=> [{:id=>1, :message=>'Bamboozle...'}]
    +
    + +And so on. Hopefully this example is clear enough. + +Now that we have a working interface to the resources in the application, we +can write a pseudo model that maintains an active client and can wrap up method +calls to appear almost like working with the real model remotely. From 7b7965642c2246acdfeace52cb09f82fdd484d4b Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 18:44:07 -0400 Subject: [PATCH 24/27] Removed strikethrough for the clients documentation which is now available. --- content/docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/index.txt b/content/docs/index.txt index 3d4c100..76772da 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -31,7 +31,7 @@ h3. Advanced Topics * "Connecting to Databases":/docs/databases.html * "Logging":/docs/logging.html -* -"Customizing Clients":/docs/clients.html- +* "Customizing Clients":/docs/clients.html * "Advanced Rack Topics":/docs/rack.html * "Exceptions":/docs/exceptions.html * -"Troubleshooting":/docs/troubleshooting.html- From 84d7025399d71469f6ddf5b0d5215d1b7e073fd6 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 18:45:51 -0400 Subject: [PATCH 25/27] Fixed inaccuracy in the client documentation. --- content/docs/clients.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/content/docs/clients.html b/content/docs/clients.html index de0a18d..2fc17e4 100644 --- a/content/docs/clients.html +++ b/content/docs/clients.html @@ -32,7 +32,11 @@ end def show - ok Message[params[:id]] + if (msg = Message[params[:id]]) + ok msg + else + raise NotFound.new + end end def create From 31af8adf1514172c5099d4bfa6c847aef6cb851d Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 19:14:54 -0400 Subject: [PATCH 26/27] Added documentation on troubleshooting. --- content/docs/index.txt | 2 +- content/docs/troubleshooting.txt | 55 +++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/content/docs/index.txt b/content/docs/index.txt index 76772da..6e0873d 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -34,7 +34,7 @@ h3. Advanced Topics * "Customizing Clients":/docs/clients.html * "Advanced Rack Topics":/docs/rack.html * "Exceptions":/docs/exceptions.html -* -"Troubleshooting":/docs/troubleshooting.html- +* "Troubleshooting":/docs/troubleshooting.html h3. Samples diff --git a/content/docs/troubleshooting.txt b/content/docs/troubleshooting.txt index c2132ff..14c419e 100644 --- a/content/docs/troubleshooting.txt +++ b/content/docs/troubleshooting.txt @@ -5,6 +5,59 @@ filter: - erb - textile --- + h2. Troubleshooting -Coming soon. +Troubleshooting Halcyon applications can be tricky, but Halcyon should be solid +enough that most exceptions raised should be caught and the error logged along +where your logger has been specified (by default to standard out) and should +keep running. This means that the log should be your first place to check for +issues in the code. + + +h3. The Console + +Halcyon applications can be run through a server or can be run through the +console interactively. To run your app this way (to hand construct requests +and prod and poke your app to find the exact point of failure), simply run: + +
    
    +$ halcyon -i
    +
    + +This will start your application in interactive mode, similar to the +@merb -i@ console. + + +h3. @500 Server Internal Error@ and Uncaught Exceptions + +Since most errors won't crash the server and will be logged, you will rarely +have to look hard to find what happened, though finding out why may be a +different matter. + +If an uncaught exception (that doesn't inherit from +@Halcyon::Exceptions::Base@) does occur, you should expect a simple response of +@500 Internal Server Error@. If this does occur, simply look through the strack +trace in the log and determine where in your code the problem exists. + + +h3. Standard Debugging Tools + +In the course of figuring out issues you may be having, don't forget about some +of the excellent debugging tools available to the Ruby community such as +@ruby-debug@ and various other debuggers. + + +h3. Bugs + +Of course, Halcyon isn't bug free so if you experience a problem with the +framework itself, please visit our "issue tracker":http://halcyon.lighthouseapp.com/projects/7222-halcyon/overview +and submit a bug report! Be sure to tag it with the @bug@ tag. + + +h2. Known Issues + +As time progresses, we will try to document known gotchas and issues you may +face while learning the ins and outs of Halcyon application development. + +* Windows users may experience a "FloatingDomainError issue":http://halcyon.lighthouseapp.com/projects/7222/tickets/46-floatdomainerror. This is resolved in 0.5.1! From 465e20fec35959182ce98e7f5d71d294c8cadf5c Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Thu, 12 Jun 2008 19:15:51 -0400 Subject: [PATCH 27/27] Fixed index listing. --- content/docs/index.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/content/docs/index.txt b/content/docs/index.txt index 6e0873d..f39e5a4 100644 --- a/content/docs/index.txt +++ b/content/docs/index.txt @@ -5,6 +5,7 @@ filter: - erb - textile --- + h2. Documentation If you're looking for thorough documentation and plenty of samples, this is where you will find what you're looking for.