mikyung 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +17 -0
- data/README.textile +149 -0
- data/Rakefile +63 -0
- data/lib/mikyung.rb +84 -0
- data/lib/opensearch_mediatype.rb +54 -0
- metadata +68 -0
data/LICENSE
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
/***
|
2
|
+
* Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
|
3
|
+
* All rights reserved.
|
4
|
+
*
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
* you may not use this file except in compliance with the License.
|
7
|
+
* You may obtain a copy of the License at
|
8
|
+
*
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
*
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
* See the License for the specific language governing permissions and
|
15
|
+
* limitations under the License.
|
16
|
+
*/
|
17
|
+
|
data/README.textile
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
h2. The need for a Rest client
|
2
|
+
|
3
|
+
*Let media types and relations* drive your client, as REST is.
|
4
|
+
|
5
|
+
But, Http, Verbs, Media types, Representations, Error codes, retrials, conditional requests, there are so many things we should do to implement a REST system.
|
6
|
+
|
7
|
+
Built upon Restfulie, *Mikyung* provides a layer of client generalization that gives you enough room to configure how your client should behave in different situations and make them work with different servers.
|
8
|
+
|
9
|
+
The following example can be executed from the example directory, just *clone and rake spec* it.
|
10
|
+
|
11
|
+
h2. Simple example
|
12
|
+
|
13
|
+
In order to create a REST client, anyone needs to have a goal and a sequence of steps that we can follow to pursue this goal. Mikyung implements a simple framework so you can define your goal and steps, and let everything else happen.
|
14
|
+
|
15
|
+
As a simple example, let's try to buy something in *any* REST server:
|
16
|
+
|
17
|
+
<pre>Mikyung.new(Buy.new).run('http://openbuystore.caelumobjects.com:/')</pre>
|
18
|
+
|
19
|
+
This is how you awake a REST client, that will start executing a series of requests until its objective is achieved.
|
20
|
+
|
21
|
+
If we want to achieve the same goal in another system, Mikyung and Restfulie will handle it all, just change the entry point URI. All its required is that your client understands the server's media types.
|
22
|
+
|
23
|
+
And now, our goal is to have a payment that is paid:
|
24
|
+
|
25
|
+
<pre>
|
26
|
+
class Buy < RelationDrivenObjective
|
27
|
+
|
28
|
+
def completed?(resource)
|
29
|
+
resource.respond_to?(:status) && resource.status == "paid"
|
30
|
+
end
|
31
|
+
|
32
|
+
executes FinishesOrder, :when => has_relation?(:payment)
|
33
|
+
executes PickProduct, :when => has_relation?(:basket)
|
34
|
+
executes SearchProducts, :when => has_relation?(:search)
|
35
|
+
|
36
|
+
end
|
37
|
+
</pre>
|
38
|
+
|
39
|
+
If there is a search relation, let's search for our items:
|
40
|
+
|
41
|
+
<pre>
|
42
|
+
class SearchProducts
|
43
|
+
def execute(entry)
|
44
|
+
entry.search.get("rest")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
</pre>
|
48
|
+
|
49
|
+
How will it work? Restfulie will content negotiate and discover that your server understands, lets say, opensearch. Using an OpenSearch media type handler, it will serialize your object to execute the query.
|
50
|
+
|
51
|
+
If your server understood Yahoo's YQL and provided an YQL specification handler, Restfulie would execute the query using YQL.
|
52
|
+
|
53
|
+
The next step is to choose the cheapest and the first product on the list:
|
54
|
+
|
55
|
+
<pre>
|
56
|
+
class PickProduct
|
57
|
+
|
58
|
+
def execute(list)
|
59
|
+
|
60
|
+
cheapest = list.entries...
|
61
|
+
|
62
|
+
# adds the cheapest and the first product
|
63
|
+
basket = [{:id => cheapest.id, :quantity => 1}, {:id => list.entries[0].id, :quantity => 1}]
|
64
|
+
|
65
|
+
list.basket.post!(basket)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
</pre>
|
69
|
+
|
70
|
+
Again, if your server implements a well known e-commerce media type, Restfulie will serialize and handle everything for you. If your server uses a custom one, you can create your own handler and contribute with our project (although REST systems should, as much as possible, stick to well known media types).
|
71
|
+
|
72
|
+
Let's buy our products:
|
73
|
+
|
74
|
+
<pre>
|
75
|
+
class FinishesOrder
|
76
|
+
def execute(basket)
|
77
|
+
payment = {:creditcard_number => "4850000000000001", :creditcard_holder => "guilherme silveira", :creditcard_expires => "10/2020", :creditcard_code => "123", :amount => basket.price}
|
78
|
+
basket.payment.post!(payment)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
</pre>
|
82
|
+
|
83
|
+
Again, media type handling, content negotiation, retrials, everything is handled by Mikyung and Restfulie.
|
84
|
+
|
85
|
+
Now you can rest... or keep reading
|
86
|
+
|
87
|
+
h2. The power
|
88
|
+
|
89
|
+
The above example should suffice to buy something in *any* REST system that contains the basket and payment process.
|
90
|
+
|
91
|
+
h2. How?
|
92
|
+
|
93
|
+
All you need to do is understand some media type that your server does. If the server understands well-known ones (as per the REST definition), contribute to Restfulie with your media type implementation.
|
94
|
+
|
95
|
+
If the server understand a custom media type, it should be their job to provide a media type handler, as they did not follow any standard.
|
96
|
+
|
97
|
+
Note that providing a media type handler does not mean that your client will follow some specific steps, but whenever it follow some steps, it will it as the media type described.
|
98
|
+
|
99
|
+
h2. Team and Support
|
100
|
+
|
101
|
+
Guilherme Silveira is behind Mikyung and support can be received through Restfulie's mailing lists.
|
102
|
+
|
103
|
+
h2. Registering media types and much more
|
104
|
+
|
105
|
+
Mikyung uses Restfulie so everything related with content type negotiation should be configured there.
|
106
|
+
|
107
|
+
h2. Objective and Steps
|
108
|
+
|
109
|
+
Everything related to objective, steps, error code handling, execution postponing can be configured or hacked into Mikyung.
|
110
|
+
|
111
|
+
h2. Objective examples
|
112
|
+
|
113
|
+
You can create your own objective builders, with the required conditions that your goal achiever requires.
|
114
|
+
Mikyung provides one objective builder called *RelationDrivenObjective* to describe how to use it. The following example shows how to achieve a buying objective:
|
115
|
+
|
116
|
+
[pre]
|
117
|
+
class Buy < RelationDrivenObjective
|
118
|
+
|
119
|
+
executes FinishesOrder, :when => has_relation(:payment)
|
120
|
+
executes PickProduct, :when => has_relation(:basket)
|
121
|
+
executes SearchProducts, :when => has_relation(:search)
|
122
|
+
|
123
|
+
def completed?(resource)
|
124
|
+
resource.respond_to?(:status) && resource.status == "paid"
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
[/pre]
|
129
|
+
|
130
|
+
|
131
|
+
A less declarative approach is to implement both the completed? and *next_step* methods:
|
132
|
+
|
133
|
+
[pre]
|
134
|
+
class Buy
|
135
|
+
|
136
|
+
def completed?(resource)
|
137
|
+
resource.respond_to?(:status) && resource.status == "paid"
|
138
|
+
end
|
139
|
+
|
140
|
+
def next_step(resource)
|
141
|
+
options = [ ["payment", FinishesOrder], ["basket", PickProduct], ["search", SearchProducts]]
|
142
|
+
step = options.find do |k|
|
143
|
+
resource.respond_to?(k.first)
|
144
|
+
end
|
145
|
+
step ? step.last : nil
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
[/pre]
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/specification'
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
|
8
|
+
GEM = "mikyung"
|
9
|
+
GEM_VERSION = "0.7.0"
|
10
|
+
SUMMARY = "Mikyung is a restful client built on top of Restfulie that allows real rest clients to be built."
|
11
|
+
AUTHOR = "Guilherme Silveira"
|
12
|
+
EMAIL = "guilherme.silveira@caelum.com.br"
|
13
|
+
HOMEPAGE = "http://restfulie.caelumobjects.com"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = GEM
|
17
|
+
s.version = GEM_VERSION
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
s.summary = SUMMARY
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
s.files = FileList['lib/**/*.rb', '[A-Z]*'].to_a
|
22
|
+
s.add_dependency("restfulie", [">= 0.7.0"])
|
23
|
+
|
24
|
+
s.author = AUTHOR
|
25
|
+
s.email = EMAIL
|
26
|
+
s.homepage = HOMEPAGE
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
30
|
+
pkg.gem_spec = spec
|
31
|
+
end
|
32
|
+
|
33
|
+
Rake::RDocTask.new("rdoc") do |rdoc|
|
34
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
35
|
+
# rdoc.rdoc_files.include('lib/**/**/*.rb')
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
require 'yard'
|
40
|
+
YARD::Rake::YardocTask.new do |t|
|
41
|
+
t.files = ['lib/restfulie/**/*.rb', 'README.textile'] # optional
|
42
|
+
# t.options = ['--any', '--extra', '--opts'] # optional
|
43
|
+
end
|
44
|
+
rescue; end
|
45
|
+
|
46
|
+
desc "Install the gem locally"
|
47
|
+
task :install => [:package] do
|
48
|
+
sh %{gem install pkg/#{GEM}-#{GEM_VERSION} -l}
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Create a gemspec file"
|
52
|
+
task :make_spec do
|
53
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
54
|
+
file.puts spec.to_ruby
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Builds the project"
|
59
|
+
task :build => :install
|
60
|
+
|
61
|
+
desc "Default build will run specs"
|
62
|
+
task :default => :install
|
63
|
+
|
data/lib/mikyung.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
class HasRelation
|
2
|
+
def initialize(rel)
|
3
|
+
@rel = rel
|
4
|
+
end
|
5
|
+
|
6
|
+
def matches?(resource)
|
7
|
+
resource.respond_to?(@rel)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class RelationDrivenObjective
|
12
|
+
|
13
|
+
def self.executes(what, options = {})
|
14
|
+
internals << [options[:when], what]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.has_relation?(rel)
|
18
|
+
HasRelation.new(rel)
|
19
|
+
end
|
20
|
+
|
21
|
+
def internals
|
22
|
+
self.class.internals
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_step(resource)
|
26
|
+
step = internals.find do |k|
|
27
|
+
k.first.matches?(resource)
|
28
|
+
end
|
29
|
+
step ? step.last : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def self.internals
|
35
|
+
@steps ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
# iterates following a series of steps provided a goal and a starting uri.
|
43
|
+
#
|
44
|
+
# Mikyung.new(objective).run(uri)
|
45
|
+
class Mikyung
|
46
|
+
|
47
|
+
# initializes with a goal in mind
|
48
|
+
def initialize(goal)
|
49
|
+
@goal = goal
|
50
|
+
end
|
51
|
+
|
52
|
+
# keeps changing from a steady state to another until its goal has been achieved
|
53
|
+
def run(uri)
|
54
|
+
|
55
|
+
current = Restfulie.at(uri).get!
|
56
|
+
Restfulie::Common::Logger.logger.debug current.response.body
|
57
|
+
|
58
|
+
while(!@goal.completed?(current))
|
59
|
+
step = @goal.next_step(current)
|
60
|
+
raise "No step was found for #{current} with links #{current.links}" unless step
|
61
|
+
Restfulie::Common::Logger.logger.debug "Mikyung > next step will be #{step}"
|
62
|
+
current = try_to_execute(step.new, current, 3)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def try_to_execute(step, current, max_attempts)
|
70
|
+
raise "Unable to proceed when trying to #{step}" if max_attempts == 0
|
71
|
+
|
72
|
+
resource = step.execute(current)
|
73
|
+
if resource.response.code != 200
|
74
|
+
try_to_execute(step, max_attempts - 1)
|
75
|
+
else
|
76
|
+
Restfulie::Common::Logger.logger.debug resource.response.body
|
77
|
+
resource
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def direct_execute(step, current, max_attempts)
|
82
|
+
step.execute(current)
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class OpenSearchEngine
|
2
|
+
|
3
|
+
def initialize(uri)
|
4
|
+
@description = Restfulie.accepts("application/opensearchdescription+xml").at(uri).get!
|
5
|
+
end
|
6
|
+
|
7
|
+
def post!(terms)
|
8
|
+
urls = @description["OpenSearchDescription"]["Url"]
|
9
|
+
uri = urls["template"].gsub("{searchTerms}", terms).gsub("{startPage?}","1")
|
10
|
+
type = urls["type"]
|
11
|
+
Restfulie.at(uri).accepts(type).get!
|
12
|
+
end
|
13
|
+
|
14
|
+
include Restfulie::Client::HTTP::RequestMarshaller
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
module Restfulie::Common::Representation
|
20
|
+
|
21
|
+
class OpenSearch
|
22
|
+
|
23
|
+
cattr_reader :media_type_name
|
24
|
+
@@media_type_name = 'application/opensearchdescription+xml'
|
25
|
+
|
26
|
+
cattr_reader :headers
|
27
|
+
@@headers = {
|
28
|
+
:get => { 'Accept' => media_type_name },
|
29
|
+
:post => { }
|
30
|
+
}
|
31
|
+
|
32
|
+
#Convert raw string to rAtom instances
|
33
|
+
def unmarshal(content)
|
34
|
+
Hash.from_xml(content)
|
35
|
+
end
|
36
|
+
|
37
|
+
def marshal(content)
|
38
|
+
content
|
39
|
+
end
|
40
|
+
|
41
|
+
# prepares a link request element based on a relation.
|
42
|
+
def prepare_link_for(link)
|
43
|
+
if link.rel=="search"
|
44
|
+
OpenSearchEngine.new(link.href)
|
45
|
+
else
|
46
|
+
link
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
Restfulie::Client::HTTP::RequestMarshaller.register_representation("application/opensearchdescription+xml", Restfulie::Common::Representation::OpenSearch)
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mikyung
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guilherme Silveira
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-04-08 00:00:00 -03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: restfulie
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.7.0
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: guilherme.silveira@caelum.com.br
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/mikyung.rb
|
35
|
+
- lib/opensearch_mediatype.rb
|
36
|
+
- LICENSE
|
37
|
+
- Rakefile
|
38
|
+
- README.textile
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://restfulie.caelumobjects.com
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Mikyung is a restful client built on top of Restfulie that allows real rest clients to be built.
|
67
|
+
test_files: []
|
68
|
+
|