mwunsch-weary 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Mark Wunsch
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Weary
2
+
3
+ _The Weary need REST_
4
+
5
+ Weary is a tiny little DSL for making the consumption of RESTful web services simple. It is the little brother to [HTTParty](http://github.com/jnunemaker/httparty/ "JNunemaker's HTTParty"). It provides a thin, gossamer-like layer over the Net/HTTP library.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+
4
+ task :default => :spec
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gemspec|
9
+ gemspec.name = "weary"
10
+ gemspec.summary = "A little DSL for consuming RESTful web services"
11
+ gemspec.email = "mark@markwunsch.com"
12
+ gemspec.homepage = "http://github.com/mwunsch/weary"
13
+ gemspec.description = "The Weary need REST: a tiny DSL that makes the consumption of RESTful web services simple."
14
+ gemspec.authors = "Mark Wunsch"
15
+ gemspec.has_rdoc = false
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
20
+
21
+
22
+
23
+ Spec::Rake::SpecTask.new do |t|
24
+ t.spec_files = FileList['spec/**/*_spec.rb']
25
+ t.spec_opts = ['--color','--format nested']
26
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,3 @@
1
+ module Weary
2
+ class HTTPError < StandardError; end
3
+ end
@@ -0,0 +1,80 @@
1
+ module Weary
2
+ class Request
3
+
4
+ attr_reader :uri
5
+ attr_accessor :options
6
+
7
+ def initialize(url, http_verb= :get, options={})
8
+ self.method = http_verb
9
+ self.uri = url
10
+ self.options = options
11
+ end
12
+
13
+ def uri=(url)
14
+ @uri = URI.parse(url)
15
+ end
16
+
17
+ def method=(http_verb)
18
+ @http_verb = case http_verb
19
+ when *Methods[:get]
20
+ :get
21
+ when *Methods[:post]
22
+ :post
23
+ when *Methods[:put]
24
+ :put
25
+ when *Methods[:delete]
26
+ :delete
27
+ when *Methods[:head]
28
+ :head
29
+ else
30
+ raise ArgumentError, "Only GET, POST, PUT, DELETE, and HEAD methods are supported"
31
+ end
32
+ end
33
+
34
+ def method
35
+ @http_verb
36
+ end
37
+
38
+ def perform
39
+ req = http.request(request)
40
+ response = Response.new(req, @http_verb)
41
+ unless options[:no_follow]
42
+ if response.redirected?
43
+ response.follow_redirect
44
+ else
45
+ response
46
+ end
47
+ else
48
+ response
49
+ end
50
+ end
51
+ alias make perform
52
+
53
+ private
54
+ def http
55
+ connection = Net::HTTP.new(@uri.host, @uri.port)
56
+ connection.use_ssl = @uri.is_a?(URI::HTTPS)
57
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if connection.use_ssl
58
+ connection
59
+ end
60
+
61
+ def request
62
+ prepare = case @http_verb
63
+ when :get
64
+ Net::HTTP::Get.new(@uri.request_uri)
65
+ when :post
66
+ Net::HTTP::Post.new(@uri.request_uri)
67
+ when :put
68
+ Net::HTTP::Put.new(@uri.request_uri)
69
+ when :delete
70
+ Net::HTTP::Delete.new(@uri.request_uri)
71
+ when :head
72
+ Net::HTTP::Head.new(@uri.request_uri)
73
+ end
74
+ prepare.body = options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] if options[:body]
75
+ prepare.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password]) if options[:basic_auth]
76
+ prepare
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+ module Weary
2
+ class Resource
3
+ attr_reader :name, :with, :requires, :via, :format, :url
4
+
5
+ def initialize(name,options={})
6
+ @domain = options[:domain]
7
+ self.name = name
8
+ self.via = options[:via]
9
+ self.with = options[:with]
10
+ self.requires = options[:requires]
11
+ self.format = options[:format]
12
+ self.url = options[:url]
13
+ @authenticates = (options[:authenticates] != false)
14
+ @follows = (options[:no_follow] == false)
15
+ end
16
+
17
+ def name=(resource)
18
+ @name = resource.to_s
19
+ end
20
+
21
+ def via=(verb)
22
+ @via = verb
23
+ end
24
+
25
+ def with=(params)
26
+ if params.empty?
27
+ @with = nil
28
+ else
29
+ @with = params.collect {|x| x.to_sym }
30
+ end
31
+ end
32
+
33
+ def url=(pattern)
34
+ if pattern.index("<domain>")
35
+ raise StandardError, "Domain flag found but the domain is not defined" if @domain.nil?
36
+ pattern = pattern.gsub("<domain>", @domain)
37
+ end
38
+ pattern = pattern.gsub("<resource>", @name)
39
+ pattern = pattern.gsub("<format>", @format.to_s)
40
+ @url = pattern
41
+ end
42
+
43
+ def requires=(params)
44
+ if (params.nil? || params.empty?)
45
+ @requires = nil
46
+ else
47
+ @requires = params
48
+ end
49
+ end
50
+
51
+ def format=(type)
52
+ @format = type
53
+ end
54
+
55
+ def authenticates?
56
+ @authenticates
57
+ end
58
+
59
+ def follows_redirects?
60
+ @follows
61
+ end
62
+
63
+ def to_hash
64
+ {@name.to_sym => {:via => @via,
65
+ :with => @with,
66
+ :requires => @requires,
67
+ :authenticates => authenticates?,
68
+ :format => @format,
69
+ :url => @url},
70
+ :no_follow => !follows_redirects?}
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,109 @@
1
+ module Weary
2
+ class Response
3
+
4
+ attr_reader :raw, :method, :code, :message, :header, :content_type, :cookie, :body
5
+ alias mime_type content_type
6
+
7
+ def initialize(http_response, http_method)
8
+ raise ArgumentError, "Must be a Net::HTTPResponse" unless http_response.is_a?(Net::HTTPResponse)
9
+ @raw = http_response
10
+ @method = http_method
11
+ @code = http_response.code.to_i
12
+ @message = http_response.message
13
+ @header = http_response.to_hash
14
+ @content_type = http_response.content_type
15
+ @cookie = http_response['Set-Cookie']
16
+ @body = http_response.body
17
+ self.format = http_response.content_type
18
+ end
19
+
20
+ def redirected?
21
+ @raw.is_a?(Net::HTTPRedirection)
22
+ end
23
+
24
+ def success?
25
+ (200..299).include?(@code)
26
+ end
27
+
28
+ def format=(type)
29
+ @format = case type
30
+ when 'text/xml', 'application/xml'
31
+ :xml
32
+ when 'application/json', 'text/json', 'application/javascript', 'text/javascript'
33
+ :json
34
+ when 'text/html'
35
+ :html
36
+ when 'application/x-yaml', 'text/yaml'
37
+ :yaml
38
+ when 'text/plain'
39
+ :plain
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ def format
46
+ @format
47
+ end
48
+
49
+ def follow_redirect
50
+ if redirected?
51
+ Request.new(@raw['location'], @method).perform
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+ def parse
58
+ raise StandardError, "The Response has no body. #{@method.to_s.upcase} request sent." unless @body
59
+ handle_errors
60
+ case @format
61
+ when :xml, :html
62
+ Crack::XML.parse @body
63
+ when :json
64
+ Crack::JSON.parse @body
65
+ when :yaml
66
+ YAML::load @body
67
+ else
68
+ @body
69
+ end
70
+ end
71
+
72
+ def search(selector)
73
+ raise ArgumentError, "Search can only be used with an XML or HTML document." unless @format != (:xml || :html)
74
+ doc = Nokogiri.parse(@body)
75
+ doc.search(selector)
76
+ end
77
+
78
+ private
79
+ def handle_errors
80
+ case @code
81
+ when 301,302
82
+ raise HTTPError, "#{@message} to #{@raw['location']}"
83
+ when 200...400
84
+ return
85
+ when 400
86
+ raise HTTPError, "Failed with #{@code}: #{@message}"
87
+ when 401
88
+ raise HTTPError, "Failed with #{@code}: #{@message}"
89
+ when 403
90
+ raise HTTPError, "Failed with #{@code}: #{@message}"
91
+ when 404
92
+ raise HTTPError, "Failed with #{@code}: #{@message}"
93
+ when 405
94
+ raise HTTPError, "Failed with #{@code}: #{@message}"
95
+ when 409
96
+ raise HTTPError, "Failed with #{@code}: #{@message}"
97
+ when 422
98
+ raise HTTPError, "Failed with #{@code}: #{@message}"
99
+ when 401...500
100
+ raise HTTPError, "Failed with #{@code}: #{@message}"
101
+ when 500...600
102
+ raise HTTPError, "Failed with #{@code}: #{@message}"
103
+ else
104
+ raise HTTPError, "Unknown response code: #{@code}"
105
+ end
106
+ end
107
+
108
+ end
109
+ end
data/lib/weary.rb ADDED
@@ -0,0 +1,150 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'net/https'
6
+
7
+ require 'rubygems'
8
+ require 'crack'
9
+
10
+ gem 'nokogiri'
11
+ autoload :Yaml, 'yaml'
12
+ autoload :Nokogiri, 'nokogiri'
13
+
14
+ require 'weary/request'
15
+ require 'weary/response'
16
+ require 'weary/resource'
17
+ require 'weary/exceptions'
18
+
19
+
20
+ module Weary
21
+
22
+ Methods = { :get => [:get, :GET, /\bget\b/i],
23
+ :post => [:post, :POST, /\bpost\b/i],
24
+ :put => [:put, :PUT, /\bput\b/i],
25
+ :delete => [:delete, :del, :DELETE, :DEL, /\bdelete\b/i],
26
+ :head => [:head, :HEAD, /\bhead\b/i] }
27
+
28
+ # Weary::Query quickly performs a :get request on a URL and parses the request
29
+ def self.Query(url)
30
+ req = Weary::Request.new(url, :get).perform
31
+ req.parse
32
+ end
33
+
34
+ attr_reader :domain, :resources
35
+
36
+ def on_domain(domain)
37
+ parse_domain = URI.extract(domain)
38
+ raise ArgumentError, 'The domain must be a URL.' if parse_domain.empty?
39
+ @domain = parse_domain[0]
40
+ end
41
+ alias domain= on_domain
42
+
43
+ def as_format(format)
44
+ @default_format = format.to_sym
45
+ end
46
+ alias format= as_format
47
+
48
+ def construct_url(pattern)
49
+ @url_pattern = pattern.to_s
50
+ end
51
+ alias url= construct_url
52
+
53
+ def authenticates_with(username,password)
54
+ @username = username
55
+ @password = password
56
+ return nil
57
+ end
58
+
59
+ def declare_resource(resource, options={})
60
+ # available options:
61
+ # :via = get, post, etc. defaults to get
62
+ # :with = paramaters passed to body or query
63
+ # :requires = members of :with that must be in the action
64
+ # :authenticates = boolean; uses basic_authentication
65
+ # :url = a pattern
66
+ # :format = to set format, defaults to :json
67
+ # :no_follow = boolean; defaults to false. do not follow redirects
68
+
69
+
70
+ @resources ||= []
71
+
72
+ r = Weary::Resource.new(resource, set_defaults(options))
73
+ declaration = r.to_hash
74
+
75
+ @resources << declaration
76
+
77
+ craft_methods(r)
78
+ return declaration
79
+ end
80
+
81
+ def get(resource,options={})
82
+ options[:via] = :get
83
+ declare_resource(resource,options)
84
+ end
85
+
86
+ def post(resource,options={})
87
+ options[:via] = :post
88
+ declare_resource(resource,options)
89
+ end
90
+
91
+ def put(resource,options={})
92
+ options[:via] = :put
93
+ declare_resource(resource,options)
94
+ end
95
+
96
+ def delete(resource,options={})
97
+ options[:via] = :delete
98
+ declare_resource(resource,options)
99
+ end
100
+
101
+ private
102
+ def set_defaults(hash)
103
+ hash[:domain] = @domain
104
+ hash[:via] ||= :get
105
+ hash[:with] ||= []
106
+ hash[:with] = hash[:with] | hash[:requires] unless hash[:requires].nil?
107
+ hash[:format] ||= (@default_format || :json)
108
+ hash[:authenticates] ||= false
109
+ hash[:authenticates] = false if hash[:authenticates] == "false"
110
+ if hash[:authenticates]
111
+ raise StandardError, "Can not authenticate unless username and password are defined" unless (@username && @password)
112
+ end
113
+ hash[:url] ||= (@url_pattern || "<domain><resource>.<format>")
114
+ hash[:no_follow] ||= false
115
+ return hash
116
+ end
117
+
118
+ def craft_methods(resource)
119
+ code = %Q{
120
+ def #{resource.name}(params={})
121
+ options ||= {}
122
+ url = "#{resource.url}"
123
+ }
124
+ unless resource.requires.nil?
125
+ resource.requires.each do |required|
126
+ code << %Q{raise ArgumentError, "This resource requires parameter: ':#{required}'" unless params.has_key?(:#{required}) \n}
127
+ end
128
+ end
129
+ unless resource.with.nil?
130
+ with = %Q\[#{resource.with.collect {|x| ":#{x}"}.join(',')}]\
131
+ code << "unnecessary = params.keys - #{with} \n"
132
+ code << "unnecessary.each { |x| params.delete(x) } \n"
133
+ end
134
+ if resource.via == (:post || :put)
135
+ code << "options[:body] = params unless params.empty? \n"
136
+ else
137
+ code << "options[:query] = params unless params.empty? \n"
138
+ code << %Q{url << "?" + options[:query].to_params unless options[:query].nil? \n}
139
+ end
140
+ if resource.authenticates?
141
+ code << %Q{options[:basic_auth] = {:username => "#{@username}", :password => "#{@password}"} \n}
142
+ end
143
+ code << %Q{
144
+ Weary::Request.new(url, :#{resource.via}, options).perform
145
+ end
146
+ }
147
+ class_eval code
148
+ end
149
+
150
+ end
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ gem 'rspec'
3
+ require 'spec'
4
+ require File.join(File.dirname(__FILE__), '../..', 'lib', 'weary')
5
+
6
+ describe Weary::Request do
7
+
8
+ it "should contain a url" do
9
+ test = Weary::Request.new("http://google.com")
10
+ test.uri.is_a?(URI).should == true
11
+ end
12
+
13
+ it "should parse the http method" do
14
+ test = Weary::Request.new("http://google.com", "POST")
15
+ test.method.should == :post
16
+ end
17
+
18
+ it "should craft a Net/HTTP Request" do
19
+ test = Weary::Request.new("http://google.com").send :http
20
+ test.class.should == Net::HTTP
21
+ end
22
+
23
+ it "should follow redirects" do
24
+ pending "Not sure how to test this"
25
+ end
26
+ end
@@ -0,0 +1,140 @@
1
+ require 'rubygems'
2
+ gem 'rspec'
3
+ require 'spec'
4
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'weary')
5
+
6
+ describe Weary do
7
+ before do
8
+ @test = Class.new
9
+ @test.instance_eval { extend Weary }
10
+ end
11
+
12
+ describe "default domain" do
13
+ it 'should be set with a url' do
14
+ @test.on_domain("http://twitter.com/")
15
+ @test.domain.should == "http://twitter.com/"
16
+ end
17
+
18
+ it "should also be set by it's alias" do
19
+ @test.domain = "http://twitter.com/"
20
+ @test.domain.should == "http://twitter.com/"
21
+ end
22
+
23
+ it 'should raise an exception when a url is not present' do
24
+ lambda { @test.on_domain("foobar") }.should raise_error
25
+ end
26
+
27
+ it 'should only take the first url given' do
28
+ @test.on_domain("with http://google.com/ and http://yahoo.com/")
29
+ @test.domain.should == "http://google.com/"
30
+ end
31
+ end
32
+
33
+ describe "default format" do
34
+ it 'can be set' do
35
+ @test.as_format("xml")
36
+ @test.instance_variable_defined?(:@default_format).should == true
37
+ end
38
+
39
+ it "should also be set by it's alias" do
40
+ @test.format = "xml"
41
+ @test.instance_variable_defined?(:@default_format).should == true
42
+ end
43
+
44
+ it 'should be a symbol' do
45
+ @test.as_format("xml")
46
+ @test.instance_variable_get(:@default_format).class.should == Symbol
47
+ end
48
+ end
49
+
50
+ describe "default url pattern" do
51
+ it 'can be set' do
52
+ @test.construct_url("<domain><resource>.<format>")
53
+ @test.instance_variable_defined?(:@url_pattern).should == true
54
+ end
55
+
56
+ it "should also be set by it's alias" do
57
+ @test.url = "<domain><resource>.<format>"
58
+ @test.instance_variable_defined?(:@url_pattern).should == true
59
+ end
60
+
61
+ it 'should be a string' do
62
+ @test.construct_url(123)
63
+ @test.instance_variable_get(:@url_pattern).class.should == String
64
+ end
65
+ end
66
+
67
+ describe "basic authentication credentials" do
68
+ it "should accept a username and password" do
69
+ @test.authenticates_with("foo","bar")
70
+ @test.instance_variable_get(:@username).should == "foo"
71
+ @test.instance_variable_get(:@password).should == "bar"
72
+ end
73
+ end
74
+
75
+ describe "resource declaration" do
76
+ before do
77
+ @test.domain = "http://twitter.com/"
78
+ end
79
+
80
+ it "should adds a new resource" do
81
+ @test.declare_resource("resource")
82
+ @test.resources[0].has_key?(:resource).should == true
83
+ end
84
+
85
+ it "should default to a GET request" do
86
+ @test.declare_resource("resource")[:resource][:via].should == :get
87
+ end
88
+
89
+ it "should default to JSON if no format is defined" do
90
+ @test.declare_resource("resource")[:resource][:format].should == :json
91
+ end
92
+
93
+ it "should use the declared format, if a specific format is not defined" do
94
+ @test.format = :xml
95
+ @test.declare_resource("resource")[:resource][:format].should == :xml
96
+ end
97
+
98
+ it "should override the default format with it's own format" do
99
+ @test.format = :xml
100
+ @test.declare_resource("resource",{:format => :yaml})[:resource][:format].should == :yaml
101
+ end
102
+
103
+ it "should form a url if there is a default pattern" do
104
+ @test.declare_resource("resource")[:resource][:url].should == "http://twitter.com/resource.json"
105
+ end
106
+
107
+ it "should override the default pattern with it's own url" do
108
+ @test.declare_resource("resource",{:url => "http://foobar.com/<resource>"})[:resource][:url].should == "http://foobar.com/resource"
109
+ end
110
+
111
+ it "should be able to contain a set of allowed parameters" do
112
+ @test.declare_resource("resource",{:with => [:id]})[:resource][:with].empty?.should == false
113
+ end
114
+
115
+ it "should be able to contain a set of required parameters" do
116
+ @test.declare_resource("resource",{:requires => [:id]})[:resource][:requires].empty?.should == false
117
+ end
118
+
119
+ it "should merge required parameters into allowed parameters" do
120
+ @test.declare_resource("resource",{:requires => [:id]})[:resource][:with].empty?.should == false
121
+ end
122
+
123
+ it "should authenticate with username and password" do
124
+ @test.authenticates_with("foo","bar")
125
+ @test.declare_resource("resource",{:authenticates => true})[:resource][:authenticates].should == true
126
+ end
127
+
128
+ it "should raise an exception if authentication is required but no credentials are supplied" do
129
+ lambda do
130
+ @test.declare_resource("resource",{:authenticates => true})
131
+ end.should raise_error
132
+ end
133
+
134
+ it "should create a method for an instantiated object" do
135
+ @test.declare_resource("resource")
136
+ @test.public_method_defined?(:resource).should == true
137
+ end
138
+
139
+ end
140
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mwunsch-weary
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mark Wunsch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: "The Weary need REST: a tiny DSL that makes the consumption of RESTful web services simple."
17
+ email: mark@markwunsch.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.md
25
+ files:
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - VERSION
30
+ - lib/weary.rb
31
+ - lib/weary/exceptions.rb
32
+ - lib/weary/request.rb
33
+ - lib/weary/resource.rb
34
+ - lib/weary/response.rb
35
+ - spec/weary/request_spec.rb
36
+ - spec/weary_spec.rb
37
+ has_rdoc: false
38
+ homepage: http://github.com/mwunsch/weary
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --charset=UTF-8
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.2.0
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: A little DSL for consuming RESTful web services
63
+ test_files:
64
+ - spec/weary/request_spec.rb
65
+ - spec/weary_spec.rb