diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1db2b94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +compiled/* +output/* 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 index 7f7d133..41907d1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,153 +1,23 @@ -$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib"))) +# $Id$ -%w(rubygems rake rake/clean rake/packagetask rake/gempackagetask rake/rdoctask rake/contrib/rubyforgepublisher fileutils pp).each{|dep|require dep} +gem 'haml', '=1.8.2' +require 'haml' -include FileUtils +load 'tasks/setup.rb' -require 'lib/halcyon' +task :default => :build -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' -} +desc 'deploy the site to the webserver' +task :deploy => [:build, 'deploy:rsync'] -BASEDIR = File.expand_path(File.dirname(__FILE__)) +# EOF -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 +namespace(:site) do - desc "run rspec" - task :run do - sh "bacon -r~/lib/bacon/output -rlib/halcyon -rspec/spec_helper spec/**/* -o CTestUnit" + desc 'Update the website' + task :update => [:build] do + `rsync -avz ./output/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/ > /dev/null` + puts "* uploaded ./output/ to http://halcyon.rubyforge.org/" 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/content/about.txt b/content/about.txt new file mode 100644 index 0000000..2c25786 --- /dev/null +++ b/content/about.txt @@ -0,0 +1,10 @@ +--- +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 0000000..129d4a2 Binary files /dev/null and b/content/css/blueprint/lib/grid.png differ 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 0000000..1514d51 Binary files /dev/null and b/content/css/blueprint/plugins/buttons/icons/cross.png differ diff --git a/content/css/blueprint/plugins/buttons/icons/key.png b/content/css/blueprint/plugins/buttons/icons/key.png new file mode 100644 index 0000000..a9d5e4f Binary files /dev/null and b/content/css/blueprint/plugins/buttons/icons/key.png differ diff --git a/content/css/blueprint/plugins/buttons/icons/tick.png b/content/css/blueprint/plugins/buttons/icons/tick.png new file mode 100644 index 0000000..a9925a0 Binary files /dev/null and b/content/css/blueprint/plugins/buttons/icons/tick.png differ diff --git a/content/css/blueprint/plugins/css-classes/Readme b/content/css/blueprint/plugins/css-classes/Readme new file mode 100644 index 0000000..9594e23 --- /dev/null +++ b/content/css/blueprint/plugins/css-classes/Readme @@ -0,0 +1,14 @@ +CSS Development Classes Plugin + +Sets up some classes to use in CSS development + +This is an experimental plugin, and the tools it provides +are not exactly semantically correct, so use with care, +and preferably only in development. :) + + +Usage +---------------------------------------------------------------- + +1) Add this line to "blueprint/screen.css", and you're done: + @import 'plugins/css-classes/css-classes.css'; diff --git a/content/css/blueprint/plugins/css-classes/css-classes.css b/content/css/blueprint/plugins/css-classes/css-classes.css new file mode 100644 index 0000000..805a38b --- /dev/null +++ b/content/css/blueprint/plugins/css-classes/css-classes.css @@ -0,0 +1,24 @@ +/* -------------------------------------------------------------- + + css-classes.css + * Classes for CSS development + + See the Readme file in this directory + for further instructions. + +-------------------------------------------------------------- */ + +.left { float:left; } +.right { float:right; } + +.hide { display:none; } + +.reset-margin { margin:0; } +.reset-padding { padding:0; } +.reset { margin:0; padding:0; } + +.align-justify { text-align:justify; } +.align-left { text-align:left; } +.align-center { text-align:center; } +.align-right { text-align:right; } + diff --git a/content/css/blueprint/plugins/fancy-type/Readme b/content/css/blueprint/plugins/fancy-type/Readme new file mode 100644 index 0000000..7d2474d --- /dev/null +++ b/content/css/blueprint/plugins/fancy-type/Readme @@ -0,0 +1,22 @@ +Fancy Type +* Gives you classes to use if you'd like some + extra fancy typography. + +Credits and instructions are specified above each class +in the fancy-type.css file in this directory. + + +Usage +---------------------------------------------------------------- + +1) Add this line to "blueprint/screen.css", and you're done: + @import 'plugins/fancy-type/fancy-type-compressed.css'; + +Note that this uses the compressed version of the CSS file, +as the original file contains a lot of instructing comments. + +Remember to re-compress (or change) the compressed file +if you make any changes to the original CSS file. + +Here's a pretty good CSS compressor: +http://teenage.cz/acidofil/tools/cssformat.php diff --git a/content/css/blueprint/plugins/fancy-type/fancy-type-compressed.css b/content/css/blueprint/plugins/fancy-type/fancy-type-compressed.css new file mode 100644 index 0000000..ac2b598 --- /dev/null +++ b/content/css/blueprint/plugins/fancy-type/fancy-type-compressed.css @@ -0,0 +1,5 @@ +p + p { text-indent:2em; margin-top:-1.5em; } +.alt{color:#666;font-family:"Warnock Pro","Goudy Old Style","Palatino","Book Antiqua",Georgia,serif;font-size:1.2em;line-height:80%;font-style:italic;} +.dquo{margin-left:-.5em;} +p.incr,.incr p{font-size:10px;line-height:1.44em;margin-bottom:1.5em;} +.caps{font-variant:small-caps;letter-spacing:1px;text-transform:lowercase;font-size:1.2em;line-height:1%;font-weight:bold;} diff --git a/content/css/blueprint/plugins/fancy-type/fancy-type.css b/content/css/blueprint/plugins/fancy-type/fancy-type.css new file mode 100644 index 0000000..51ce464 --- /dev/null +++ b/content/css/blueprint/plugins/fancy-type/fancy-type.css @@ -0,0 +1,74 @@ +/* -------------------------------------------------------------- + + fancy-type.css + * Lots of pretty advanced classes for manipulating text. + + See the Readme file in this folder for additional instructions. + +-------------------------------------------------------------- */ + +/* Indentation instead of line shifts for sibling paragraphs. */ + p + p { text-indent:2em; margin-top:-1.5em; } + +/* Ornaments on first paragraph. + Commented out by default. Use with care. + p:before { content: "\2767"; padding-right: 0.4em; } + p + p:before { content: ""; padding:0; } */ + +/* For great looking type, use this code instead of asdf: + asdf + 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/content/css/styles.sass b/content/css/styles.sass new file mode 100644 index 0000000..446a3ce --- /dev/null +++ b/content/css/styles.sass @@ -0,0 +1,223 @@ +--- +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). + +!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 diff --git a/content/docs/clients.html b/content/docs/clients.html new file mode 100644 index 0000000..2fc17e4 --- /dev/null +++ b/content/docs/clients.html @@ -0,0 +1,218 @@ +--- +title: Docs — Customizing Clients +layout: simple +filter: + - erb + - textile +--- + +h2. Customizing Clients + +*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 + if (msg = Message[params[:id]]) + ok msg + else + raise NotFound.new + end + 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. 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/controllers.html b/content/docs/controllers.html new file mode 100644 index 0000000..f5f4ac0 --- /dev/null +++ b/content/docs/controllers.html @@ -0,0 +1,133 @@ +--- +title: Docs — Writing Controllers +layout: simple +filter: + - erb + - textile +--- + +h2. Writing Controllers + +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 + + 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/databases.txt b/content/docs/databases.txt new file mode 100644 index 0000000..66b1c68 --- /dev/null +++ b/content/docs/databases.txt @@ -0,0 +1,236 @@ +--- +title: Docs — Connecting to Databases +layout: simple +filter: + - erb + - textile +--- + +h2. Connecting to Databases + +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, + :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(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 +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/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/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..f39e5a4 --- /dev/null +++ b/content/docs/index.txt @@ -0,0 +1,51 @@ +--- +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. 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 +* "Troubleshooting":/docs/troubleshooting.html + + +h3. Samples + +* -"Aurora":/docs/samples/aurora.html- +* -"Guesser":/docs/samples/guesser.html- +* -"Ranger":/docs/samples/ranger.html- +* "WeeDB":/docs/samples/weedb.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..6e34400 --- /dev/null +++ b/content/docs/installation.txt @@ -0,0 +1,71 @@ +--- +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
+ +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 + +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..da76d05 --- /dev/null +++ b/content/docs/introduction.html @@ -0,0 +1,138 @@ +--- +title: Docs — Introduction to Halcyon +layout: simple +filter: + - 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. + + +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. + +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 + +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. + +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. + +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. + +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. + + +h2. 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. + +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 + +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. + + + +h3. 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":http://wikipedia.org/wiki/REST. + + +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 +# Powerful Abstractions + +Rack's design elegantly shows off the power of its simplicity. + + +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. + + +h2. 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. + + +h2. 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/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. 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/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/routes.html b/content/docs/routes.html new file mode 100644 index 0000000..2707b1c --- /dev/null +++ b/content/docs/routes.html @@ -0,0 +1,142 @@ +--- +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/init/routes.rb@, wherein you will find +something like this by default: + +<% coderay(:lang => "ruby", :line_numbers => "inline", :tab_width => 2) do -%> +# = 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). + +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 Links section for more +information. + + +h3. Resources + +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')@. + +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: + +* "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/ 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/docs/troubleshooting.txt b/content/docs/troubleshooting.txt new file mode 100644 index 0000000..14c419e --- /dev/null +++ b/content/docs/troubleshooting.txt @@ -0,0 +1,63 @@ +--- +title: Docs — Troubleshooting +layout: simple +filter: + - erb + - textile +--- + +h2. Troubleshooting + +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! diff --git a/content/docs/tutorial.txt b/content/docs/tutorial.txt new file mode 100644 index 0000000..19d0806 --- /dev/null +++ b/content/docs/tutorial.txt @@ -0,0 +1,226 @@ +--- +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  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. + +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. + +*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: + +
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-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. + +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"=>"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. + +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. + + +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. + +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 Model.columns + end + def create + msg = Message.create(params.merge(:tags => params[:tags].join)) + msg.save + ok msg.id + end + def read + ok Message[params[:id]] + end + def update + Message.filter(:id => params[:id]).update(params) + ok + end + def delete + Message.filter(:id => params[:id]).delete + ok + end +end +<% end -%> + +@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. + +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 +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 + +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/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/img/bullet.gif b/content/img/bullet.gif new file mode 100644 index 0000000..45bb956 Binary files /dev/null and b/content/img/bullet.gif differ diff --git a/content/img/h_lcyon_large.png b/content/img/h_lcyon_large.png new file mode 100644 index 0000000..43ce1b7 Binary files /dev/null and b/content/img/h_lcyon_large.png differ diff --git a/content/img/h_lcyon_small.png b/content/img/h_lcyon_small.png new file mode 100644 index 0000000..81cf69e Binary files /dev/null and b/content/img/h_lcyon_small.png differ diff --git a/content/img/h_lcyon_small_i.png b/content/img/h_lcyon_small_i.png new file mode 100644 index 0000000..ecf404f Binary files /dev/null and b/content/img/h_lcyon_small_i.png differ 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 0000000..c82b0e6 Binary files /dev/null and b/content/img/h_lcyon_small_i_x150.png differ diff --git a/content/img/halcyon.png b/content/img/halcyon.png new file mode 100644 index 0000000..f57b4eb Binary files /dev/null and b/content/img/halcyon.png differ diff --git a/content/img/halcyon_i.png b/content/img/halcyon_i.png new file mode 100644 index 0000000..3419623 Binary files /dev/null and b/content/img/halcyon_i.png differ diff --git a/content/img/halcyon_is.png b/content/img/halcyon_is.png new file mode 100644 index 0000000..a2c66c3 Binary files /dev/null and b/content/img/halcyon_is.png differ diff --git a/content/img/halcyon_r.png b/content/img/halcyon_r.png new file mode 100644 index 0000000..2ec11d9 Binary files /dev/null and b/content/img/halcyon_r.png differ diff --git a/content/img/halcyon_s.png b/content/img/halcyon_s.png new file mode 100644 index 0000000..756c083 Binary files /dev/null and b/content/img/halcyon_s.png differ diff --git a/content/img/halcyon_title.png b/content/img/halcyon_title.png new file mode 100644 index 0000000..6d29270 Binary files /dev/null and b/content/img/halcyon_title.png differ diff --git a/content/img/logo.jpg b/content/img/logo.jpg new file mode 100644 index 0000000..444f8f2 Binary files /dev/null and b/content/img/logo.jpg differ diff --git a/content/img/top_bg.gif b/content/img/top_bg.gif new file mode 100644 index 0000000..fb13d3f Binary files /dev/null and b/content/img/top_bg.gif differ diff --git a/content/index.txt b/content/index.txt new file mode 100644 index 0000000..fca4e01 --- /dev/null +++ b/content/index.txt @@ -0,0 +1,106 @@ +--- +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 + +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? 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. + + + +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 Message.columns + end + def create + msg = Message.create(params) + msg.save + ok msg.id + end + def read + ok Message[params[:id]] + end + def update + Message.filter(:id => params[:id]).update(params) + ok + end + def delete + Message.filter(:id => params[:id]).delete + ok + end +end +<% end -%> + +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":/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. + +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. + +For your viewing pleasure, here are some of the Ohloh project factoids: + + 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/layouts/default.rhtml b/layouts/default.rhtml new file mode 100644 index 0000000..7be6049 --- /dev/null +++ b/layouts/default.rhtml @@ -0,0 +1,93 @@ +--- +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. + + #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. + %p It is ideal for creating light-weight service application layers, such as APIs for existing apps, etc. + + %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 + :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."); + 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'} 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'} 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/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 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.