halcyon 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,135 @@
1
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "lib")))
2
+
3
+ %w(rubygems rake rake/clean rake/packagetask rake/gempackagetask rake/rdoctask rake/contrib/rubyforgepublisher fileutils pp).each{|dep|require dep}
4
+
5
+ include FileUtils
6
+
7
+ require 'halcyon'
8
+
9
+ project = {
10
+ :name => "halcyon",
11
+ :version => Halcyon.version,
12
+ :author => "Matt Todd",
13
+ :email => "chiology@gmail.com",
14
+ :description => "A JSON App Server Framework",
15
+ :homepath => 'http://halcyon.rubyforge.org',
16
+ :bin_files => %w(halcyon),
17
+ :rdoc_files => %w(lib),
18
+ :rdoc_opts => %w[
19
+ --all
20
+ --quiet
21
+ --op rdoc
22
+ --line-numbers
23
+ --inline-source
24
+ --title "Halcyon\ documentation"
25
+ --exclude "^(_darcs|spec|pkg)/"
26
+ ]
27
+ }
28
+
29
+ BASEDIR = File.expand_path(File.dirname(__FILE__))
30
+
31
+ spec = Gem::Specification.new do |s|
32
+ s.name = project[:name]
33
+ s.version = project[:version]
34
+ s.platform = Gem::Platform::RUBY
35
+ s.has_rdoc = true
36
+ s.extra_rdoc_files = project[:rdoc_files]
37
+ s.rdoc_options += project[:rdoc_opts]
38
+ s.summary = project[:description]
39
+ s.description = project[:description]
40
+ s.author = project[:author]
41
+ s.email = project[:email]
42
+ s.homepage = project[:homepath]
43
+ s.executables = project[:bin_files]
44
+ s.bindir = "bin"
45
+ s.require_path = "lib"
46
+ s.add_dependency('rack', '>=0.2.0')
47
+ s.add_dependency('json', '>=1.1.1')
48
+ s.add_dependency('merb', '>=0.4.1')
49
+ s.required_ruby_version = '>= 1.8.6'
50
+ s.files = (project[:rdoc_files] + %w[Rakefile] + Dir["{spec,lib}/**/*"]).uniq
51
+ end
52
+
53
+ Rake::GemPackageTask.new(spec) do |p|
54
+ p.need_zip = true
55
+ p.need_tar = true
56
+ end
57
+
58
+ desc "Package and Install halcyon"
59
+ task :install do
60
+ name = "#{project[:name]}-#{project[:version]}.gem"
61
+ sh %{rake package}
62
+ sh %{sudo gem install pkg/#{name}}
63
+ end
64
+
65
+ desc "Uninstall the halcyon gem"
66
+ task :uninstall => [:clean] do
67
+ sh %{sudo gem uninstall #{project[:name]}}
68
+ end
69
+
70
+ task 'run-spec' do
71
+ require 'spec'
72
+ $:.unshift(File.dirname(__FILE__))
73
+ stdout = []
74
+ class << stdout
75
+ def print(*e) concat(e); Kernel.print(*e); end
76
+ def puts(*e) concat(e); Kernel.puts(*e); end
77
+ def flush; end
78
+ end
79
+ stderr = []
80
+ class << stderr
81
+ alias print <<
82
+ def print(*e) concat(e); Kernel.print(*e); end
83
+ def puts(*e) concat(e); Kernel.puts(*e); end
84
+ def flush; end
85
+ end
86
+ ::Spec::Runner::CommandLine.run(['spec'], stderr, stdout, false, true)
87
+ exit_status = stdout.last.strip[/(\d+) failures?/, 1].to_i
88
+ at_exit{
89
+ exit(exit_status == 0 ? 0 : 1)
90
+ }
91
+ end
92
+
93
+ desc "run rspec"
94
+ task :spec do
95
+ run = Rake::Task['run-spec']
96
+ run.execute
97
+ end
98
+
99
+ task :default => :spec
100
+
101
+ desc "Do predistribution stuff"
102
+ task :predist => [:chmod, :changelog, :rdoc]
103
+
104
+ def manifest
105
+ `darcs query manifest`.split("\n").map { |f| f.gsub(/\A\.\//, '') }
106
+ end
107
+
108
+ desc "Make binaries executable"
109
+ task :chmod do
110
+ Dir["bin/*"].each { |binary| File.chmod(0775, binary) }
111
+ Dir["test/cgi/test*"].each { |binary| File.chmod(0775, binary) }
112
+ end
113
+
114
+ desc "Generate a ChangeLog"
115
+ task :changelog do
116
+ sh "svn log >ChangeLog"
117
+ end
118
+
119
+ desc "Generate RDoc documentation"
120
+ Rake::RDocTask.new(:rdoc) do |rdoc|
121
+ rdoc.options << '--line-numbers' << '--inline-source' <<
122
+ '--main' << 'README' <<
123
+ '--title' << 'Halcyon Documentation' <<
124
+ '--charset' << 'utf-8'
125
+ rdoc.rdoc_dir = "doc"
126
+ rdoc.rdoc_files.include 'README'
127
+ rdoc.rdoc_files.include('lib/halcyon.rb')
128
+ rdoc.rdoc_files.include('lib/halcyon/*.rb')
129
+ rdoc.rdoc_files.include('lib/halcyon/*/*.rb')
130
+ end
131
+
132
+ task :pushsite => [:rdoc] do
133
+ sh "rsync -avz doc/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/doc/"
134
+ sh "rsync -avz site/ mtodd@halcyon.rubyforge.org:/var/www/gforge-projects/halcyon/"
135
+ end
data/bin/halcyon ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby -wKU
2
+ #--
3
+ # Created by Matt Todd on 2007-10-25.
4
+ # Copyright (c) 2007. All rights reserved.
5
+ #++
6
+
7
+ # Blatantly stolen from Chris Neukirchen's rackup utility for running Rack
8
+ # apps. (Forgive me, it just made too much sense to use your Rack bootstrap
9
+ # code for my Rack bootstrap.)
10
+
11
+ #--
12
+ # dependencies
13
+ #++
14
+
15
+ %w(optparse).each{|dep|require dep}
16
+
17
+ #--
18
+ # default options
19
+ #++
20
+
21
+ $debug = false
22
+ options = {
23
+ :environment => 'none',
24
+ :port => 9267,
25
+ :host => 'localhost',
26
+ :server => 'mongrel',
27
+ :log_file => '/tmp/halcyon.log'
28
+ }
29
+
30
+ #--
31
+ # parse options
32
+ #++
33
+
34
+ opts = OptionParser.new("", 24, ' ') do |opts|
35
+ opts.banner << "Halcyon, JSON Server Framework\n"
36
+ opts.banner << "http://halcyon.rubyforge.org/\n"
37
+ opts.banner << "\n"
38
+ opts.banner << "Usage: halcyon [options] appname"
39
+
40
+ opts.separator ""
41
+ opts.separator "Options:"
42
+
43
+ opts.on("-d", "--debug", "set debugging flags (set $debug to true)") { $debug = true }
44
+ opts.on("-w", "--warn", "turn warnings on for your script") { $-w = true }
45
+
46
+ opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path|
47
+ $:.unshift(*path.split(":"))
48
+ end
49
+
50
+ opts.on("-r", "--require LIBRARY", "require the library, before executing your script") do |library|
51
+ require library
52
+ end
53
+
54
+ opts.on("-c", "--config PATH", "configuration stored in PATH") do |conf|
55
+ options[:config_file] = conf
56
+ end
57
+
58
+ opts.on("-s", "--server SERVER", "serve using SERVER (default: #{options[:server]})") do |serv|
59
+ options[:server] = serv
60
+ end
61
+
62
+ opts.on("-o", "--host HOST", "listen on HOST (default: #{options[:host]})") do |host|
63
+ options[:host] = host
64
+ end
65
+
66
+ opts.on("-p", "--port PORT", "use PORT (default: #{options[:port]})") do |port|
67
+ options[:port] = port
68
+ end
69
+
70
+ opts.on("-l", "--logfile PATH", "log access to PATH (default: #{options[:log_file]})") do |log_file|
71
+ options[:log_file] = log_file
72
+ end
73
+
74
+ opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{options[:environment]})") do |env|
75
+ options[:environment] = env
76
+ end
77
+
78
+ opts.on_tail("-h", "--help", "Show this message") do
79
+ puts opts
80
+ exit
81
+ end
82
+
83
+ opts.on_tail("-v", "--version", "Show version") do
84
+ # require 'halcyon'
85
+ puts "Halcyon #{Halcyon::Server.version}"
86
+ exit
87
+ end
88
+
89
+ begin
90
+ opts.parse! ARGV
91
+ rescue OptionParser::InvalidOption => e
92
+ abort "You used an unsupported option. Try: halcyon -h"
93
+ end
94
+ end
95
+
96
+ abort "Halcyon needs an app to run. Try: halcyon -h" if ARGV.empty?
97
+ options[:app] = ARGV.shift
98
+
99
+ #--
100
+ # load dependencies
101
+ #++
102
+
103
+ %w(rubygems rack).each{|dep|require dep}
104
+
105
+ $:.unshift '/Users/mtodd/Sites/halcyon/trunk/lib/'
106
+ %w(halcyon/server).each {|dep|require dep}
107
+
108
+ #--
109
+ # load app
110
+ #++
111
+
112
+ if !File.exists?("#{options[:app]}.rb")
113
+ abort "Halcyon did not find the app #{options[:app]}. Check your path and try again."
114
+ end
115
+
116
+ require options[:app]
117
+ app = Object.const_get(File.basename(options[:app]).capitalize.gsub(/_([a-z])/){|m|m[1].chr.capitalize})
118
+
119
+ #--
120
+ # prepare server
121
+ #++
122
+
123
+ require options[:server]
124
+ server = Rack::Handler.const_get(options[:server].capitalize)
125
+
126
+ #--
127
+ # prepare app environment
128
+ #++
129
+
130
+ case options[:environment]
131
+ when "development"
132
+ app = Rack::Builder.new {
133
+ use Rack::CommonLogger, STDERR unless server.name =~ /CGI/
134
+ use Rack::ShowExceptions
135
+ use Rack::Reloader
136
+ use Rack::Lint
137
+ run app.new(options)
138
+ }.to_app
139
+ when "deployment"
140
+ app = Rack::Builder.new {
141
+ use Rack::CommonLogger, STDERR unless server.name =~ /CGI/
142
+ run app.new(options)
143
+ }.to_app
144
+ else
145
+ app = app.new(options)
146
+ end
147
+
148
+ #--
149
+ # start server
150
+ #++
151
+
152
+ server.run app, :Port => options[:port]
data/lib/halcyon.rb ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Created by Matt Todd on 2007-12-14.
4
+ # Copyright (c) 2007. All rights reserved.
5
+ #++
6
+
7
+ $:.unshift File.dirname(__FILE__)
8
+
9
+ #--
10
+ # dependencies
11
+ #++
12
+
13
+ %w(halcyon/support/hashext).each {|dep|require dep}
14
+
15
+ class Hash
16
+ include HashExt::Keys
17
+ end
18
+
19
+ #--
20
+ # module
21
+ #++
22
+
23
+ module Halcyon
24
+ VERSION = [0,3,7]
25
+ def self.version
26
+ VERSION.join('.')
27
+ end
28
+
29
+ # = Introduction
30
+ #
31
+ # Halcyon is a JSON Web Server Framework intended to be used for fast, small
32
+ # data transactions, like for AJAX-intensive sites or for special services like
33
+ # authentication centralized for numerous web apps in the same cluster.
34
+ #
35
+ # The possibilities are pretty limitless: the goal of Halcyon was simply to be
36
+ # lightweight, fast, simple to implement and use, and able to be extended.
37
+ #
38
+ # == Usage
39
+ #
40
+ # For documentation on using Halcyon, check out the Halcyon::Server::Base and
41
+ # Halcyon::Client::Base classes which contain much more usage documentation.
42
+ def introduction
43
+ abort "READ THE DAMNED RDOCS FOO"
44
+ end
45
+
46
+ #--
47
+ # module dependencies
48
+ #++
49
+
50
+ autoload :Exceptions, 'halcyon/exceptions'
51
+ autoload :Server, 'halcyon/server'
52
+ autoload :Client, 'halcyon/client'
53
+
54
+ end
@@ -0,0 +1,43 @@
1
+ #--
2
+ # Created by Matt Todd on 2007-12-14.
3
+ # Copyright (c) 2007. All rights reserved.
4
+ #++
5
+
6
+ $:.unshift File.dirname(File.join('..', __FILE__))
7
+ $:.unshift File.dirname(__FILE__)
8
+
9
+ #--
10
+ # dependencies
11
+ #++
12
+
13
+ %w(halcyon rubygems json).each {|dep|require dep}
14
+
15
+ #--
16
+ # module
17
+ #++
18
+
19
+ module Halcyon
20
+
21
+ # The Client library provides a simple way to package up a client lib to
22
+ # simplify communicating with the accompanying Halcyon server app.
23
+ #
24
+ # = Usage
25
+ #
26
+ # For documentation on using Halcyon, check out the Halcyon::Server::Base and
27
+ # Halcyon::Client::Base classes which contain much more usage documentation.
28
+ class Client
29
+ VERSION.replace [0,2,12]
30
+ def self.version
31
+ VERSION.join('.')
32
+ end
33
+
34
+ #--
35
+ # module dependencies
36
+ #++
37
+
38
+ autoload :Base, 'halcyon/client/base'
39
+ autoload :Exceptions, 'halcyon/client/exceptions'
40
+ autoload :Router, 'halcyon/client/router'
41
+
42
+ end
43
+ end
@@ -0,0 +1,236 @@
1
+ #--
2
+ # Created by Matt Todd on 2007-12-14.
3
+ # Copyright (c) 2007. All rights reserved.
4
+ #++
5
+
6
+ #--
7
+ # dependencies
8
+ #++
9
+
10
+ %w(net/http uri json).each {|dep|require dep}
11
+
12
+ #--
13
+ # module
14
+ #++
15
+
16
+ module Halcyon
17
+ class Client
18
+
19
+ DEFAULT_OPTIONS = {}
20
+ USER_AGENT = "JSON/#{JSON::VERSION} Compatible (en-US) Halcyon/#{Halcyon.version} Client/#{Halcyon::Client.version}"
21
+ CONTENT_TYPE = 'application/json'
22
+
23
+ # = Building Custom Clients
24
+ #
25
+ # Once your Halcyon JSON Server App starts to take shape, it may be useful
26
+ # to begin to write tests on expected functionality, and then to implement
27
+ # API calls with a designated Client lib for your Ruby or Rails apps, etc.
28
+ # The Base class provides a standard implementation and several options for
29
+ # wrapping up functionality of your app from the server side into the
30
+ # client side so that you may begin to use response data.
31
+ #
32
+ # == Creating Your Client
33
+ #
34
+ # Creating a simple client can be as simple as this:
35
+ #
36
+ # class Simple < Halcyon::Client::Base
37
+ # def greet(name)
38
+ # get("/hello/#{name}")
39
+ # end
40
+ # end
41
+ #
42
+ # The only thing simply may be actually using the Simple client you just
43
+ # created.
44
+ #
45
+ # But to actually get in and use the library, one has to take full
46
+ # advantage of the HTTP request methods, +get+, +post+, +put+, and
47
+ # +delete+. These methods simply return the JSON-parsed data from the
48
+ # server, effectively returning a hash with two key values, +status+ which
49
+ # contains the HTTP status code, and +body+ which contains the body of the
50
+ # content returned which can be any number of objects, including, but not
51
+ # limited to Hash, Array, Numeric, Nil, Boolean, String, etc.
52
+ #
53
+ # You are not limited to what your methods can call: they are arbitrarily
54
+ # and solely up to your whims and needs. It is simply a matter of good
55
+ # design and performance when it comes to structuring and implementing
56
+ # client actions which can be complex or simple series of requests to the
57
+ # server.
58
+ #
59
+ # == Acceptable Clients
60
+ #
61
+ # The Halcyon Server is intended to be very picky with whom it will speak
62
+ # to, so it requires that we specifically mention that we speak only
63
+ # "application/html", that we're "JSON/1.1.1 Compatible", and that we're
64
+ # local to the server itself (in process, anyways). This ensures that it
65
+ # has to deal with as little noise as possible and focus it's attention on
66
+ # performing our requests.
67
+ #
68
+ # This shouldn't affect usage when working with the Client or in production
69
+ # but might if you're trying to check things in your browser. Just make
70
+ # certain that the debug option is turned on (-d for the +halcyon+ command)
71
+ # when you start the server so that it knows to be a little more lenient
72
+ # about to whom it speaks.
73
+ class Base
74
+
75
+ #--
76
+ # Initialization and setup
77
+ #++
78
+
79
+ # = Connecting to the Server
80
+ #
81
+ # Creates a new Client object to allow for requests and responses from
82
+ # the specified server.
83
+ #
84
+ # The +uri+ param contains the URL to the actual server, and should be in
85
+ # the format: "http://localhost:3801" or "http://app.domain.com:3401/"
86
+ #
87
+ # == Server Connections
88
+ #
89
+ # Connecting only occurs at the actual event that a request is performed,
90
+ # so there is no need to worry about closing connections or managing
91
+ # connections in general other than good object housecleaning. (Be nice
92
+ # to your Garbage Collector.)
93
+ #
94
+ # == Usage
95
+ #
96
+ # You can either provide a block to perform all of your requests and
97
+ # processing inside of or you can simply accept the object in response
98
+ # and call your request methods off of the returned object.
99
+ #
100
+ # Alternatively, you could do both.
101
+ #
102
+ # An example of creating and using a Simple client:
103
+ #
104
+ # class Simple < Halcyon::Client::Base
105
+ # def greet(name)
106
+ # get("/hello/#{name}")
107
+ # end
108
+ # end
109
+ # Simple.new('http://localhost:3801') do |s|
110
+ # puts s.greet("Johnny").inspect
111
+ # end
112
+ #
113
+ # This should effectively call +inspect+ on a response hash similar to
114
+ # this:
115
+ #
116
+ # {:status => 200, :body => 'Hello Johnny'}
117
+ #
118
+ # Alternatively, you could perform the same with the following:
119
+ #
120
+ # s = Simple.new('http://localhost:3801')
121
+ # puts s.greet("Johnny").inspect
122
+ #
123
+ # This should generate the exact same outcome as the previous example,
124
+ # except that it is not executed in a block.
125
+ #
126
+ # The differences are purely semantic and of personal taste.
127
+ def initialize(uri)
128
+ @uri = URI.parse(uri)
129
+ if block_given?
130
+ yield self
131
+ end
132
+ end
133
+
134
+ #--
135
+ # Reverse Routing
136
+ #++
137
+
138
+ # = Reverse Routing
139
+ #
140
+ # The concept of writing our Routes in our Client is to be able to
141
+ # automatically generate the appropriate URL based on the hash given
142
+ # and where it was called from. This makes writing actions in Clients
143
+ # go from something like this:
144
+ #
145
+ # def greet(name)
146
+ # get("/hello/#{name}")
147
+ # end
148
+ #
149
+ # to this:
150
+ #
151
+ # def greet(name)
152
+ # get(url_for(__method__, :name))
153
+ # end
154
+ #
155
+ # This doesn't immediately seem to be beneficial, but it is better for
156
+ # automating URL generating, taking out the hardcoding, and has room to
157
+ # to improve in the future.
158
+ def url_for(action, params = {})
159
+ Halcyon::Client::Router.route(action, params)
160
+ end
161
+
162
+ # Sets up routing for creating preparing +url_for+ URLs. See the
163
+ # +url_for+ method documentation and the Halcyon::Client::Router docs.
164
+ def self.route
165
+ if block_given?
166
+ Halcyon::Client::Router.prepare do |r|
167
+ Halcyon::Client::Router.default_to yield(r)
168
+ end
169
+ else
170
+ warn "Routes should be defined in a block."
171
+ end
172
+ end
173
+
174
+ #--
175
+ # Request Handling
176
+ #++
177
+
178
+ # Performs a GET request on the URI specified.
179
+ def get(uri)
180
+ req = Net::HTTP::Get.new(uri)
181
+ req["Content-Type"] = CONTENT_TYPE
182
+ req["User-Agent"] = USER_AGENT
183
+ request(req)
184
+ end
185
+
186
+ # Performs a POST request on the URI specified.
187
+ def post(uri, data)
188
+ req = Net::HTTP::Post.new(uri)
189
+ req["Content-Type"] = CONTENT_TYPE
190
+ req["User-Agent"] = USER_AGENT
191
+ req.body = data.to_json
192
+ request(req)
193
+ end
194
+
195
+ # Performs a DELETE request on the URI specified.
196
+ def delete(uri)
197
+ req = Net::HTTP::Delete.new(uri)
198
+ req["Content-Type"] = CONTENT_TYPE
199
+ req["User-Agent"] = USER_AGENT
200
+ request(req)
201
+ end
202
+
203
+ # Performs a PUT request on the URI specified.
204
+ def put(uri, data)
205
+ req = Net::HTTP::Put.new(uri)
206
+ req["Content-Type"] = CONTENT_TYPE
207
+ req["User-Agent"] = USER_AGENT
208
+ req.body = data.to_json
209
+ request(req)
210
+ end
211
+
212
+ private
213
+
214
+ # Performs an arbitrary HTTP request, receive the response, parse it with
215
+ # JSON, and return it to the caller. This is a private method because the
216
+ # user/developer should be quite satisfied with the +get+, +post+, +put+,
217
+ # and +delete+ methods.
218
+ def request(req)
219
+ # prepare and send HTTP request
220
+ res = Net::HTTP.start(@uri.host, @uri.port) {|http|http.request(req)}
221
+ body = JSON.parse(res.body)
222
+ body.symbolize_keys! if body.respond_to? :symbolize_keys!
223
+
224
+ # handle non-successes
225
+ raise Halcyon::Client::Base::Exceptions.lookup(body[:status]).new unless res.kind_of? Net::HTTPSuccess
226
+
227
+ # parse response
228
+ body
229
+ rescue Halcyon::Client::Base::Exceptions::Base => e
230
+ puts "#{e.status}: #{e.error}"
231
+ end
232
+
233
+ end
234
+
235
+ end
236
+ end