restfolia 1.0.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.
@@ -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!"