restfolia 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ module Restfolia::HTTP
2
+
3
+ # Public: Wraps Net::HTTP interface.
4
+ class Request
5
+
6
+ # Public: Do a HTTP Request.
7
+ #
8
+ # method - HTTP verb to be used. Options: :get, :post, :put, :delete
9
+ # url - a String to request. (ex: http://fake.com/service)
10
+ # args - Hash options to build request (default: {}):
11
+ # :query - String to be set with url (optional).
12
+ # :body - String to be set with request (optional).
13
+ # :headers - Hash with headers to be sent in request (optional).
14
+ #
15
+ # Returns an instance of Net::HTTPResponse.
16
+ #
17
+ # Raises URI::InvalidURIError if url attribute is invalid.
18
+ def self.do_request(method, url, args = {})
19
+ query = args[:query]
20
+ body = args[:body]
21
+
22
+ uri = URI.parse(url)
23
+ uri.query = query if query
24
+
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+ verb = case method
27
+ when :get
28
+ Net::HTTP::Get.new(uri.request_uri)
29
+ when :post
30
+ Net::HTTP::Post.new(uri.request_uri)
31
+ when :put
32
+ Net::HTTP::Put.new(uri.request_uri)
33
+ when :delete
34
+ Net::HTTP::Delete.new(uri.request_uri)
35
+ else
36
+ msg = "Method have to be one of: :get, post, :put, :delete"
37
+ raise ArgumentError, msg
38
+ end
39
+ verb.body = body if body
40
+ if (headers = args[:headers])
41
+ headers.each do |header, value|
42
+ verb[header] = value
43
+ end
44
+ end
45
+ if (cookies = args[:cookies])
46
+ verb["Cookie"] = cookies
47
+ end
48
+
49
+ http.request(verb)
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,135 @@
1
+ module Restfolia
2
+
3
+ # Public: Store and execute behaviours defined by user. Behaviour is action
4
+ # for one or more HTTP code. See "default behaviours" below.
5
+ #
6
+ # Examples
7
+ #
8
+ # default behaviours
9
+ # behaviours do
10
+ #
11
+ # # 2xx
12
+ # on(200...300) do |http_response|
13
+ # content_type = (http_response["content-type"] =~ /application\/json/)
14
+ # if !content_type
15
+ # msg_error = "Response \"content-type\" header should be \"application/json\""
16
+ # raise Restfolia::ResponseError.new(msg_error, caller, http_response)
17
+ # end
18
+ #
19
+ # http_body = http_response.body.to_s
20
+ # if !http_body.empty?
21
+ # json_parsed = helpers.parse_json(http_response)
22
+ # Restfolia.create_resource(json_parsed)
23
+ # elsif (location = http_response["location"])
24
+ # helpers.follow_url(location)
25
+ # else
26
+ # nil
27
+ # end
28
+ # end
29
+ #
30
+ # # 3xx
31
+ # on(300...400) do |http_response|
32
+ # if (location = http_response["location"])
33
+ # helpers.follow_url(location)
34
+ # else
35
+ # msg_error = "HTTP status #{http_response.code} not supported"
36
+ # raise Restfolia::ResponseError.new(msg_error, caller, http_response)
37
+ # end
38
+ # end
39
+ #
40
+ # # 4xx
41
+ # on(400...500) do |http_response|
42
+ # raise Restfolia::ResponseError.new("Resource not found.",
43
+ # caller, http_response)
44
+ # end
45
+ #
46
+ # # 5xx
47
+ # on(500...600) do |http_response|
48
+ # raise Restfolia::ResponseError.new("Internal Server Error",
49
+ # caller, http_response)
50
+ # end
51
+ #
52
+ # end
53
+ #
54
+ module HTTP
55
+ autoload :Behaviour, "restfolia/http/behaviour"
56
+ autoload :Configuration, "restfolia/http/configuration"
57
+ autoload :Request, "restfolia/http/request"
58
+
59
+ # Public: Execute behaviour from HTTP Response code.
60
+ #
61
+ # http_response - Net::HTTPResponse with code attribute.
62
+ #
63
+ # Returns value from "block behaviour".
64
+ def self.response_by_status_code(http_response)
65
+ Behaviour.store.execute(http_response)
66
+ end
67
+
68
+ # Public: It's a nice way to define configurations for
69
+ # your behaves using a block.
70
+ #
71
+ # block - Required block to customize your behaves. Below are
72
+ # the methods available inside block:
73
+ # #on - See #on
74
+ #
75
+ # Examples
76
+ #
77
+ # Restfolia::HTTP.behaviours do
78
+ # on(200) { '200 behaviour' }
79
+ # on([201, 204]) { 'behaviour for 201 and 204 codes' }
80
+ # on(300...400) { '3xx range' }
81
+ # end
82
+ #
83
+ # Returns nothing.
84
+ def self.behaviours(&block)
85
+ Behaviour.store.behaviours(&block)
86
+ end
87
+
88
+ #default behaviours
89
+ behaviours do
90
+
91
+ # 2xx
92
+ on(200...300) do |http_response|
93
+ content_type = (http_response["content-type"] =~ /application\/json/)
94
+ if !content_type
95
+ msg_error = "Response \"content-type\" header should be \"application/json\""
96
+ raise Restfolia::ResponseError.new(msg_error, caller, http_response)
97
+ end
98
+
99
+ http_body = http_response.body.to_s
100
+ if !http_body.empty?
101
+ json_parsed = helpers.parse_json(http_response)
102
+ Restfolia.create_resource(json_parsed)
103
+ elsif (location = http_response["location"])
104
+ helpers.follow_url(location)
105
+ else
106
+ nil
107
+ end
108
+ end
109
+
110
+ # 3xx
111
+ on(300...400) do |http_response|
112
+ if (location = http_response["location"])
113
+ helpers.follow_url(location)
114
+ else
115
+ msg_error = "HTTP status #{http_response.code} not supported"
116
+ raise Restfolia::ResponseError.new(msg_error, caller, http_response)
117
+ end
118
+ end
119
+
120
+ # 4xx
121
+ on(400...500) do |http_response|
122
+ raise Restfolia::ResponseError.new("Resource not found.",
123
+ caller, http_response)
124
+ end
125
+
126
+ # 5xx
127
+ on(500...600) do |http_response|
128
+ raise Restfolia::ResponseError.new("Internal Server Error",
129
+ caller, http_response)
130
+ end
131
+
132
+ end
133
+ end
134
+
135
+ end
@@ -0,0 +1,109 @@
1
+ module Restfolia
2
+
3
+ # Public: Resource is the representation of JSON response. It transforms
4
+ # all JSON attributes in attributes and provides a "links" method, to
5
+ # help with hypermedia navigation.
6
+ #
7
+ # Examples
8
+ #
9
+ # resource = Resource.new(:attr_test => "test")
10
+ # resource.attr_test # => "test"
11
+ # resource.links # => []
12
+ #
13
+ # resource = Resource.new(:attr_test => "test",
14
+ # :links => {:href => "http://service.com",
15
+ # :rel => "self",
16
+ # :type => "application/json"})
17
+ # resource.attr_test # => "test"
18
+ # resource.links # => [#<Restfolia::EntryPoint ...>]
19
+ #
20
+ # By default, "links" method, expects from JSON to be the following formats:
21
+ #
22
+ # # Array de Links
23
+ # "links" : [{ "href" : "http://fakeurl.com/some/service",
24
+ # "rel" : "self",
25
+ # "type" : "application/json"
26
+ # }]
27
+ #
28
+ # # OR 'single' Links
29
+ # "links" : { "href" : "http://fakeurl.com/some/service",
30
+ # "rel" : "self",
31
+ # "type" : "application/json"
32
+ # }
33
+ #
34
+ # # OR node 'Link', that can be Array or single too
35
+ # "link" : { "href" : "http://fakeurl.com/some/service",
36
+ # "rel" : "self",
37
+ # "type" : "application/json"
38
+ # }
39
+ #
40
+ class Resource
41
+
42
+ # Public: Returns the Hash that represents parsed JSON.
43
+ attr_reader :_json
44
+
45
+ # Public: Initialize a Resource.
46
+ #
47
+ # json - Hash that represents parsed JSON.
48
+ #
49
+ # Raises ArgumentError if json parameter is not a Hash object.
50
+ def initialize(json)
51
+ unless json.is_a?(Hash)
52
+ raise(ArgumentError, "json parameter have to be a Hash object", caller)
53
+ end
54
+ @_json = json
55
+
56
+ #Add json keys as methods of Resource
57
+ #http://blog.jayfields.com/2008/02/ruby-replace-methodmissing-with-dynamic.html
58
+ @_json.each do |method, value|
59
+ next if self.respond_to?(method) #avoid method already defined
60
+
61
+ (class << self; self; end).class_eval do
62
+ define_method(method) do |*args|
63
+ value
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # Public: Read links from Resource. Links are optional.
70
+ # See Resource root doc for details.
71
+ #
72
+ # rel - Optional String parameter. Filter links by rel attribute.
73
+ #
74
+ # Returns Empty Array or Array of EntryPoints, if "rel" is informed
75
+ # it returns nil or an instance of EntryPoint.
76
+ def links(rel = nil)
77
+ @links ||= parse_links(@_json)
78
+
79
+ return nil if @links.empty? && !rel.nil?
80
+ return @links if @links.empty? || rel.nil?
81
+
82
+ @links.detect { |ep| ep.rel == rel }
83
+ end
84
+
85
+ protected
86
+
87
+ # Internal: Parse links from hash. Always normalize to return
88
+ # an Array of EntryPoints. Check if link has :href and :rel
89
+ # keys.
90
+ #
91
+ # Returns Array of EntryPoints or Empty Array if :links not exist.
92
+ # Raises RuntimeError if link doesn't have :href and :rel keys.
93
+ def parse_links(json)
94
+ links = json[:links] || json[:link]
95
+ return [] if links.nil?
96
+
97
+ links = [links] unless links.is_a?(Array)
98
+ links.map do |link|
99
+ if link[:href].nil? || link[:rel].nil?
100
+ msg = "Invalid hash link: #{link.inspect}"
101
+ raise(RuntimeError, msg, caller)
102
+ end
103
+ EntryPoint.new(link[:href], link[:rel])
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,97 @@
1
+ module Restfolia
2
+
3
+ # Public: Call a Factory of Resources. This is a good place to override and
4
+ # returns a custom Resource Factory. By default, Restfolia uses
5
+ # Restfolia::ResourceCreator.
6
+ #
7
+ # json - Hash parsed from Response body.
8
+ #
9
+ # Returns Resource instance, configured at ResourceCreator.
10
+ # Raises ArgumentError if json is not a Hash.
11
+ def self.create_resource(json)
12
+ @creator ||= Restfolia::ResourceCreator.new
13
+ @creator.create(json)
14
+ end
15
+
16
+ # Public: Factory of Resources. It transforms all JSON objects in Resources.
17
+ #
18
+ # Examples
19
+ #
20
+ # factory = Restfolia::ResourceCreator.new
21
+ # resource = factory.create(:attr_test => "test",
22
+ # :attr_tags => ["tag1", "tag2"],
23
+ # :attr_array_obj => [{:nested => "nested"}],
24
+ # :links => [{:href => "http://service.com",
25
+ # :rel => "contacts",
26
+ # :type => "application/json"},
27
+ # {:href => "http://another.com",
28
+ # :rel => "relations",
29
+ # :type => "application/json"}
30
+ # ])
31
+ # resource.attr_test # => "test"
32
+ # resource.attr_tags # => ["tag1", "tag2"]
33
+ # resource.attr_array_obj # => [#<Restfolia::Resource ...>]
34
+ #
35
+ class ResourceCreator
36
+
37
+ # Public: By default, returns Restfolia::Resource. You can use
38
+ # this method to override and returns a custom Resource. See examples.
39
+ #
40
+ # Examples
41
+ #
42
+ # # using a custom Resource
43
+ # class Restfolia::ResourceCreator
44
+ # def resource_class
45
+ # OpenStruct #dont forget to require 'ostruct'
46
+ # end
47
+ # end
48
+ #
49
+ # Returns class of Resource to be constructed.
50
+ def resource_class
51
+ Restfolia::Resource
52
+ end
53
+
54
+ # Public: creates Resource looking recursively for JSON
55
+ # objects and transforming in Resources. To create Resource,
56
+ # this method use #resource_class.new(json).
57
+ #
58
+ # json - Hash parsed from Response body.
59
+ #
60
+ # Returns Resource from #resource_class.
61
+ # Raises ArgumentError if json is not a Hash.
62
+ def create(json)
63
+ unless json.is_a?(Hash)
64
+ raise(ArgumentError, "JSON parameter have to be a Hash object", caller)
65
+ end
66
+
67
+ json_parsed = {}
68
+ json.each do |attr, value|
69
+ json_parsed[attr] = look_for_resource(value)
70
+ end
71
+ resource_class.new(json_parsed)
72
+ end
73
+
74
+ protected
75
+
76
+ # Internal: Check if value is eligible to become a Restfolia::Resource.
77
+ # If value is Array object, looks inner contents, using rules below.
78
+ # If value is Hash object, it becomes a Restfolia::Resource.
79
+ # Else return itself.
80
+ #
81
+ # value - object to be checked.
82
+ #
83
+ # Returns value itself or Resource.
84
+ def look_for_resource(value)
85
+ if value.is_a?(Array)
86
+ value = value.inject([]) do |resources, array_obj|
87
+ resources << look_for_resource(array_obj)
88
+ end
89
+ elsif value.is_a?(Hash)
90
+ value = resource_class.new(value)
91
+ end
92
+ value
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,3 @@
1
+ module Restfolia
2
+ VERSION = "1.0.0"
3
+ end
data/lib/restfolia.rb ADDED
@@ -0,0 +1,97 @@
1
+ require "net/http"
2
+ require "uri"
3
+
4
+ require "rubygems"
5
+ require "multi_json"
6
+
7
+ require "restfolia/version"
8
+ require "restfolia/exceptions"
9
+ require "restfolia/http"
10
+ require "restfolia/entry_point"
11
+ require "restfolia/resource_creator"
12
+ require "restfolia/resource"
13
+
14
+ # Public: Restfolia: a REST client to consume and interact with Hypermedia API.
15
+ #
16
+ # Against the grain, Restfolia is very opinionated about some REST's concepts:
17
+ # * Aims only *JSON Media Type*.
18
+ # * All responses are parsed and returned as Restfolia::Resource.
19
+ # * Less is more. Restfolia is very proud to be small, easy to maintain and evolve.
20
+ # * Restfolia::Resource is Ruby object with attributes from JSON and can optionally contains *hypermedia links* which have to be a specific format. See the examples below.
21
+ # * All code is very well documented, using "TomDoc":http://tomdoc.org style.
22
+ #
23
+ # Obs: This is a draft version. Not ready for production (yet!).
24
+ #
25
+ # References
26
+ #
27
+ # You can find more information about arquitecture REST below:
28
+ # * "Roy Fielding's":http://roy.gbiv.com/untangled see this post for example: "REST APIs must be hypertext-driven":http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
29
+ # * "Rest in Practice":http://restinpractice.com, especially the chapter titled "Hypermedia Formats".
30
+ # * "Mike Amundsen's Blog":http://amundsen.com/blog/
31
+ # * ROAR - "Resource Oriented Arquitectures in Ruby":https://github.com/apotonick/roar it seems really good to build a hypermedia API, of course you can go with Sinatra+rabl solutions too.
32
+ #
33
+ # Examples
34
+ #
35
+ # # GET http://localhost:9292/recursos/busca
36
+ # { "itens_por_pagina" : 10,
37
+ # "paginal_atual" : 1,
38
+ # "paginas_totais" : 1,
39
+ # "query" : "",
40
+ # "total_resultado" : 100,
41
+ # "resultado" : [ { "id" : 1,
42
+ # "name" : "Test1",
43
+ # "links" : [ { "href" : "http://localhost:9292/recursos/id/1",
44
+ # "rel" : "recurso",
45
+ # "type" : "application/json"
46
+ # } ]
47
+ # },
48
+ # { "id" : 2,
49
+ # "name" : "Test2",
50
+ # "links" : [ { "href" : "http://localhost:9292/recursos/id/2",
51
+ # "rel" : "recurso",
52
+ # "type" : "application/json"
53
+ # } ]
54
+ # }
55
+ # ],
56
+ # "links" : { "href" : "http://localhost:9292/recursos/busca",
57
+ # "rel" : "self",
58
+ # "type" : "application/json"
59
+ # },
60
+ # }
61
+ #
62
+ # # GET http://localhost:9292/recursos/id/1
63
+ # { "id" : 1,
64
+ # "name" : "Test1",
65
+ # "links" : { "href" : "http://localhost:9292/recursos/id/1",
66
+ # "rel" : "self",
67
+ # "type" : "application/json"
68
+ # }
69
+ # }
70
+ #
71
+ # # getting a resource
72
+ # resource = Restfolia.at('http://localhost:9292/recursos/busca').get
73
+ # resource.pagina_atual # => 1
74
+ # resource.resultado # => [#<Resource ...>, #<Resource ...>]
75
+ #
76
+ # # example of hypermedia navigation
77
+ # r1 = resource.resultado.first
78
+ # r1 = r1.links("recurso").get # => #<Resource ...>
79
+ # r1.name # => "Test1"
80
+ #
81
+ module Restfolia
82
+
83
+ # Public: Start point for getting the first Resource.
84
+ #
85
+ # url - String with the address of service to be accessed.
86
+ #
87
+ # Examples
88
+ #
89
+ # entry_point = Restfolia.at("http://localhost:9292/recursos/busca")
90
+ # entry_point.get # => #<Resource ...>
91
+ #
92
+ # Returns Restfolia::EntryPoint object.
93
+ def self.at(url)
94
+ EntryPoint.new(url)
95
+ end
96
+
97
+ end
data/restfolia.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "restfolia/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "restfolia"
7
+ s.version = Restfolia::VERSION
8
+ s.authors = `git log --raw | grep Author: | awk -F ': | <|>' '{ print $2 }' | sort | uniq`.split("\n")
9
+ s.email = ["roger.barreto@gmail.com"]
10
+ s.homepage = "http://rogerleite.github.com/restfolia"
11
+ s.summary = %q{REST client to consume and interact with Hypermedia API}
12
+ s.description = %q{REST client to consume and interact with Hypermedia API}
13
+ s.license = 'MIT'
14
+
15
+ s.rubyforge_project = "restfolia"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency "multi_json", "~> 1.3.0"
23
+
24
+ s.add_development_dependency "rake"
25
+ s.add_development_dependency "minitest"
26
+ s.add_development_dependency "minitest-reporters"
27
+ s.add_development_dependency "webmock"
28
+ end
@@ -0,0 +1,32 @@
1
+
2
+ # Run this sample from root project:
3
+ # $ ruby samples/changing_behaviour.rb
4
+
5
+ require "rubygems"
6
+ $LOAD_PATH << "lib"
7
+ require "restfolia"
8
+
9
+ begin
10
+ # Default behaviour to redirect 3xx is raise Error
11
+ Restfolia.at("http://google.com.br").get
12
+ rescue Restfolia::ResponseError => ex
13
+ puts ex.message
14
+ end
15
+
16
+ module Restfolia::HTTP::Behaviour
17
+
18
+ # We can change behaviour of many HTTP status
19
+ # on_2xx(http_response)
20
+ # on_3xx(http_response)
21
+ # on_4xx(http_response)
22
+ # on_5xx(http_response)
23
+
24
+ # Here we change 3xx behaviour to return a Resource
25
+ def on_3xx(http_response)
26
+ Restfolia.create_resource(:redirected => "I'm free! :D")
27
+ end
28
+
29
+ end
30
+
31
+ resource = Restfolia.at("http://google.com.br").get
32
+ puts resource.redirected
@@ -0,0 +1,38 @@
1
+
2
+ # Run this sample from root project:
3
+ # $ ruby samples/changing_links_parse.rb
4
+
5
+ require "rubygems"
6
+ $LOAD_PATH << "lib"
7
+ require "restfolia"
8
+
9
+ resource = Restfolia::Resource.new(:attr_test => "test",
10
+ :custom_links => [
11
+ {:url => "http://test.com", :rel => "rel"}
12
+ ])
13
+ puts resource.links.inspect # => []
14
+
15
+ # Now let's change the way Resource parses links
16
+ class Restfolia::Resource
17
+
18
+ def parse_links(json)
19
+ links = json[:custom_links]
20
+ return [] if links.nil?
21
+
22
+ links = [links] unless links.is_a?(Array)
23
+ links.map do |link|
24
+ if link[:url].nil? || link[:rel].nil?
25
+ msg = "Invalid hash link: #{link.inspect}"
26
+ raise(RuntimeError, msg, caller)
27
+ end
28
+ Restfolia::EntryPoint.new(link[:url], link[:rel])
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ resource = Restfolia::Resource.new(:attr_test => "test",
35
+ :custom_links => [
36
+ {:url => "http://test.com", :rel => "rel"}
37
+ ])
38
+ puts resource.links.inspect # => [#<Restfolia::EntryPoint ...>]
@@ -0,0 +1,23 @@
1
+
2
+ # Run this sample from root project:
3
+ # $ ruby samples/cookies_options.rb
4
+
5
+ require "rubygems"
6
+ $LOAD_PATH << "lib"
7
+ require "restfolia"
8
+
9
+ SERVICE_URL = "http://localhost:9292/recursos/busca"
10
+
11
+ sample_cookie = "PREF=ID=988f14fa5edd3243:TM=1335470032:LM=1335470032:S=KVBslNbyz6bG0DqU; expires=Sat, 26-Apr-2014 19:53:52 GMT; path=/; domain=.google.com, NID=59=peUyZQuLWQ_0gELr1yDf0FT4ZlT7ZdITNrO5OhkEnAvp_8MZ4TT6pHq7_q_Su-puTw7vGml_Ok6du8fLreGHzfpMs4Qh1v-qBCFYGuCNbzpwN670x5MFbGKy0KUXA3WP; expires=Fri, 26-Oct-2012 19:53:52 GMT; path=/; domain=.google.com; HttpOnly"
12
+
13
+ # accessing cookies attribute
14
+ entry_point = Restfolia.at(SERVICE_URL)
15
+ entry_point.cookies = sample_cookie
16
+ resource = entry_point.get
17
+
18
+ # adding in a fancy way
19
+ resource = Restfolia.at(SERVICE_URL).
20
+ set_cookies(sample_cookie).get
21
+
22
+ puts "Done!"
23
+
@@ -0,0 +1,27 @@
1
+
2
+ # Run this sample from root project:
3
+ # $ ruby samples/headers_options.rb
4
+
5
+ require "rubygems"
6
+ $LOAD_PATH << "lib"
7
+ require "restfolia"
8
+
9
+ SERVICE_URL = "http://localhost:9292/recursos/busca"
10
+
11
+ # accessing headers attribute
12
+ entry_point = Restfolia.at(SERVICE_URL)
13
+ entry_point.headers["ContentyType"] = "application/json"
14
+ resource = entry_point.get
15
+
16
+ # adding in a fancy way
17
+ resource = Restfolia.at(SERVICE_URL).
18
+ with_headers("Content-Type" => "application/json",
19
+ "Accept" => "application/json").
20
+ get
21
+
22
+ # helper that sets Content-Type and Accept headers
23
+ resource = Restfolia.at(SERVICE_URL).
24
+ as("application/json").
25
+ get
26
+
27
+ puts "Done!"