commute 0.1.2 → 0.2.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.todo +15 -0
  2. data/Gemfile +5 -2
  3. data/commute.gemspec +2 -1
  4. data/examples/gist_api.rb +71 -0
  5. data/examples/highrise_task_api.rb +59 -0
  6. data/examples/pastie_api.rb +18 -0
  7. data/lib/commute/aspects/caching.rb +37 -0
  8. data/lib/commute/aspects/crud.rb +41 -0
  9. data/lib/commute/aspects/pagination.rb +16 -0
  10. data/lib/commute/aspects/url.rb +57 -0
  11. data/lib/commute/common/basic_auth.rb +20 -0
  12. data/lib/commute/common/cache.rb +43 -0
  13. data/lib/commute/common/chemicals.rb +39 -0
  14. data/lib/commute/common/conditional.rb +27 -0
  15. data/lib/commute/common/em-synchrony_adapter.rb +29 -0
  16. data/lib/commute/common/em_http_request_adapter.rb +57 -0
  17. data/lib/commute/common/json.rb +28 -0
  18. data/lib/commute/common/typhoeus_adapter.rb +40 -0
  19. data/lib/commute/common/xml.rb +7 -0
  20. data/lib/commute/configuration.rb +8 -0
  21. data/lib/commute/core/api.rb +116 -0
  22. data/lib/commute/core/builder.rb +261 -0
  23. data/lib/commute/core/commuter.rb +116 -0
  24. data/lib/commute/core/context.rb +63 -0
  25. data/lib/commute/core/processors/code_status_processor.rb +40 -0
  26. data/lib/commute/core/processors/hook.rb +14 -0
  27. data/lib/commute/core/processors/request_builder.rb +26 -0
  28. data/lib/commute/core/processors/sequencer.rb +46 -0
  29. data/lib/commute/core/request.rb +58 -0
  30. data/lib/commute/core/response.rb +18 -0
  31. data/lib/commute/core/sequence.rb +180 -0
  32. data/lib/commute/core/stack.rb +145 -0
  33. data/lib/commute/version.rb +1 -1
  34. data/lib/commute.rb +4 -2
  35. data/spec/commute/aspects/caching_spec.rb +12 -0
  36. data/spec/commute/aspects/url_spec.rb +61 -0
  37. data/spec/commute/core/api_spec.rb +70 -0
  38. data/spec/commute/core/builder_spec.rb +123 -0
  39. data/spec/commute/core/commuter_spec.rb +64 -0
  40. data/spec/commute/core/processors/code_status_processor_spec.rb +5 -0
  41. data/spec/commute/core/processors/hook_spec.rb +25 -0
  42. data/spec/commute/core/processors/request_builder_spec.rb +25 -0
  43. data/spec/commute/core/processors/sequencer_spec.rb +33 -0
  44. data/spec/commute/core/sequence_spec.rb +190 -0
  45. data/spec/commute/core/stack_spec.rb +96 -0
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +73 -18
  48. data/lib/commute/adapters/typhoeus.rb +0 -13
  49. data/lib/commute/api.rb +0 -86
  50. data/lib/commute/context.rb +0 -154
  51. data/lib/commute/layer.rb +0 -16
  52. data/lib/commute/stack.rb +0 -104
  53. data/spec/commute/api_spec.rb +0 -97
  54. data/spec/commute/context_spec.rb +0 -140
  55. data/spec/commute/layer_spec.rb +0 -22
  56. data/spec/commute/stack_spec.rb +0 -125
data/.todo ADDED
@@ -0,0 +1,15 @@
1
+ conditional sequencer (body), routing on constants
2
+ conditional processors instead of only sequencers
3
+ easy transforms, reactivity? When not to add a transform
4
+ disabling
5
+ groups of commuters (Group and delaying)
6
+ queueing
7
+ batching/grouping
8
+ net/http
9
+ stubbing
10
+ gzip
11
+ resourcing (commute-resourcing)
12
+ Api::Metal
13
+ caching
14
+ calling processor from within commuter
15
+ simpler sequencer internal working
data/Gemfile CHANGED
@@ -3,5 +3,8 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in commute.gemspec
4
4
  gemspec
5
5
 
6
- # Partially solves https://github.com/typhoeus/typhoeus/pull/171.
7
- gem 'typhoeus', :git => 'https://github.com/challengee/typhoeus.git'
6
+ group :development do
7
+ gem 'dominance'
8
+ gem 'active_support'
9
+ gem 'builder'
10
+ end
data/commute.gemspec CHANGED
@@ -14,7 +14,8 @@ Gem::Specification.new do |s|
14
14
  s.require_paths = ["lib"]
15
15
  s.version = Commute::VERSION
16
16
 
17
- s.add_dependency 'typhoeus'
17
+ s.add_dependency 'typhoeus', '0.5.0.rc'
18
+ s.add_dependency 'em-http-request'
18
19
 
19
20
  s.add_development_dependency 'rake'
20
21
  s.add_development_dependency 'mocha'
@@ -0,0 +1,71 @@
1
+ require 'commute'
2
+ require 'commute/common/json'
3
+ require 'commute/common/basic_auth'
4
+
5
+ class GistApi < Commute::Api
6
+
7
+ transform do |request, context|
8
+ if context[:auth][:username]
9
+ request.url = "https://api.github.com/gists"
10
+ else
11
+ request.url = "https://api.github.com/#{context[:auth][:username]}/gists"
12
+ end
13
+ request.url << "/#{context[:id]}" if context[:id]
14
+ end
15
+
16
+ using(:request) do
17
+ append Commute::Common::Json::Render.new
18
+ append Commute::Common::BasicAuth.new
19
+ end
20
+
21
+ using(:response) do
22
+ append Commute::Common::Json::Parse.new
23
+ end
24
+
25
+ def find id = nil
26
+ # Id for url.
27
+ where id: id if id
28
+ # Get method.
29
+ get
30
+ end
31
+
32
+ def update gist = nil
33
+ body(gist).patch
34
+ end
35
+
36
+ def create gist = nil
37
+ body(gist).post
38
+ end
39
+
40
+ private
41
+
42
+ def body gist = nil
43
+ with gist: gist if gist
44
+ transform(:gist) { |request, gist| request.body = gist }
45
+ end
46
+ end
47
+
48
+ ID = '46ecd83c5e13cca5a18e'
49
+
50
+ api = GistApi.with auth: {
51
+ username: 'challengee',
52
+ password: 'challengeez0'
53
+ }
54
+
55
+ # gist, status = api.find(ID).run
56
+ # p status
57
+
58
+ # gist[:description] = 'cool'
59
+
60
+ gist = {
61
+ :public => false
62
+ :files => {
63
+ "file1.txt" => {
64
+ :content => 'test'
65
+ }
66
+ }
67
+ }
68
+
69
+ gist, status = api.create(gist).run
70
+ p gist
71
+ p status
@@ -0,0 +1,59 @@
1
+ require 'uri'
2
+
3
+ require 'commute'
4
+
5
+ require 'commute/aspects/url'
6
+ require 'commute/aspects/crud'
7
+ require 'commute/common/basic_auth'
8
+
9
+ require 'active_support/core_ext/hash/conversions'
10
+
11
+ class HighriseTaskApi < Commute::Api
12
+ include Commute::Aspect::Url
13
+ include Commute::Aspect::Crud
14
+
15
+ using(:request) do
16
+ append Proc.new { |c|
17
+ c.get.body = c.get.body.to_xml(root: 'task').gsub("\n", '') if c.get.body
18
+ }
19
+ append Commute::Common::BasicAuth.new
20
+ end
21
+
22
+ using(:response) do
23
+ # append Proc.new { |c|
24
+ # c.get.body = Array.from_xml(c.get.body)['tasks'] if c.get.body
25
+ # }
26
+ end
27
+
28
+ url 'https://$account.highrisehq.com/$scope/tasks/$id.xml'
29
+
30
+ transform(:task) do |request, context|
31
+ request.body = context[:task]
32
+ end
33
+ # transform :task => :body
34
+
35
+ transform { |request| request.headers['Content-Type'] = 'application/xml' }
36
+
37
+ def update task = nil
38
+ super.transform { |request| request.query['reload'] = 'true' }
39
+ end
40
+ end
41
+
42
+ api = HighriseTaskApi.with account: 'piepetest', auth: {
43
+ username: '610bf60020bb861761f07feef88ba42d',
44
+ password: 'X'
45
+ }
46
+
47
+ task = {
48
+ body: 'Kmoe iets doen'
49
+ }
50
+
51
+ task, status = api.all.run
52
+ p task.size
53
+ p status
54
+
55
+ p 'AGAIN ------------------'
56
+
57
+ task, status = api.all.run
58
+ p task.size
59
+ p status
@@ -0,0 +1,18 @@
1
+ require 'commute'
2
+
3
+ class PastieApi < Commute::Api
4
+
5
+ transform(:id) do |request, id|
6
+ request.url = "http://pastie.org/pastes/#{id}/download"
7
+ end
8
+
9
+ def find id = nil
10
+ # Id for url.
11
+ where id: id if id
12
+ # Get method.
13
+ get
14
+ end
15
+ end
16
+
17
+ pastie, status = PastieApi.find(5119067).run.data
18
+ # p pastie
@@ -0,0 +1,37 @@
1
+ require 'commute/common/cache'
2
+ require 'commute/common/conditional'
3
+
4
+ module Commute
5
+ module Aspect
6
+
7
+ # Public: Enables caching in your api by inserting
8
+ # cache processors in the api stack.
9
+ #
10
+ # Requires you to have a processor called request, before
11
+ # which the fetch processor will be inserted.
12
+ #
13
+ module Caching
14
+
15
+ def self.included api
16
+ # Insert a processor that fetches response from the cache.
17
+ api.builder.using(:request) do
18
+ insert Common::Cache::Fetch.new
19
+ end
20
+
21
+ # Insert a processor that inserts responses in the cache.
22
+ api.builder.using(:response) do
23
+ append Common::Cache::Store.new
24
+ end
25
+
26
+ # Wrap some phases so that they will only be executed on a miss.
27
+ api.builder.using do
28
+ [:execution, :response, :status].each do |id|
29
+ sequence.replace id do |processor|
30
+ Common::Conditional::NotTagged.new(processor, :hit)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ module Commute
2
+ module Aspect
3
+
4
+ module Crud
5
+
6
+ def self.included base
7
+ base.builder.transform(:body) do |request, resource|
8
+ request.body = resource
9
+ end
10
+ end
11
+
12
+ def all
13
+ get.multiple
14
+ end
15
+
16
+ def find id = nil
17
+ get.where(id: id).single
18
+ end
19
+
20
+ def update body = nil
21
+ put.with(body: body).single
22
+ end
23
+
24
+ def create body = nil
25
+ post.with(body: body).single
26
+ end
27
+
28
+ def destroy id = nil
29
+ delete.where(id: id).single
30
+ end
31
+
32
+ def single
33
+ self
34
+ end
35
+
36
+ def multiple
37
+ self
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ module Commute
2
+ module Aspect
3
+
4
+ module Pagination
5
+
6
+ def offset n = 0
7
+ end
8
+
9
+ def limit n = 1
10
+ end
11
+
12
+ def next limit = 0
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module Commute
2
+ module Aspect
3
+
4
+ module Url
5
+
6
+ def self.included base
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def url pattern
12
+ transform &generate(pattern)
13
+ end
14
+
15
+ private
16
+
17
+ def generate pattern
18
+ # Regex to match different parts of a URL.
19
+ matcher = \
20
+ /(?<scheme>https?):\/\/(?<host>[^\/:]+):?(?<port>\d+)?(?<path>[^\.]*)(?<extension>\..+)?/
21
+ # Match the pattern.
22
+ matched = matcher.match(pattern)
23
+ raise 'Invalid url: At least a host and scheme required' \
24
+ unless matched && matched[:host] && matched[:scheme]
25
+ # Collect the parts.
26
+ parts = {
27
+ scheme: matched[:scheme],
28
+ host: matched[:host].split('.'),
29
+ port: matched[:port],
30
+ path: matched[:path].split('/'),
31
+ extension: matched[:extension]
32
+ }
33
+
34
+ Proc.new do |request, context|
35
+ request.scheme = parts[:scheme]
36
+ request.host = fill(parts[:host], context).join '.'
37
+ request.path = fill(parts[:path], context).join('/') + (parts[:extension] || '')
38
+ request.port = matched[:port]
39
+ end
40
+ end
41
+
42
+ def fill parts, params
43
+ new_parts = []
44
+ parts.each do |part|
45
+ if part.start_with?('$')
46
+ param = params[part[1..-1].to_sym]
47
+ new_parts << param if param
48
+ else
49
+ new_parts << part
50
+ end
51
+ end
52
+ new_parts
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ require 'base64'
2
+
3
+ module Commute
4
+ module Common
5
+
6
+ class BasicAuth
7
+ @id = :auth
8
+
9
+ def call commuter, options = {}
10
+ commuter.change do |request|
11
+ # Ruby's Base64 puts newlines at the end (and every 60 chars)...
12
+ # This breaks HTTP. So strip them!
13
+ authorization = Base64.encode64("#{options[:username]}:#{options[:password]}}").gsub "\n", ''
14
+ request.headers['Authorization'] = "Basic #{authorization}"
15
+ request
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ module Commute
2
+ module Common
3
+
4
+ module Cache
5
+
6
+ class Success
7
+
8
+ def success?
9
+ true
10
+ end
11
+ end
12
+
13
+ SUCCESS = Success.new
14
+
15
+ class Fetch
16
+ @id = :cache
17
+
18
+ def call commuter, cache = nil
19
+ return unless cache
20
+ request = commuter.get
21
+ # Hit.
22
+ if request.method == :get && result = cache.get(request.url)
23
+ commuter.set [result, SUCCESS]
24
+ commuter.tag :hit
25
+ commuter.return
26
+ end
27
+ end
28
+ end
29
+
30
+ class Store
31
+ @id = :cache
32
+
33
+ def call commuter, cache = nil
34
+ return unless cache
35
+ response = commuter.get
36
+ if response.request.method == :get && response.body
37
+ cache.set response.request.url, response.body
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ require 'chemicals'
2
+
3
+ module Commute
4
+ module Common
5
+ class Chemicals
6
+ @id = :chemicals
7
+
8
+ def initialize root = nil
9
+ @root = root
10
+ @cache = {}
11
+ end
12
+
13
+ def call commuter, path
14
+ # Build the path to the template.
15
+ path = if @root
16
+ "#{@root}/#{path}.xml"
17
+ else
18
+ "#{path}.xml"
19
+ end
20
+ # Create/Cache the Template.
21
+ template = if @cache[path]
22
+ @cache[path]
23
+ else
24
+ ::Chemicals::Template.new(IO.read(path)).tap do |t|
25
+ @cache[path] = t
26
+ end
27
+ end
28
+ # Use the template to parse/render.
29
+ if commuter.get.body
30
+ if commuter.get.body.kind_of?(String) && !commuter.get.body.strip.empty?
31
+ commuter.get.body = template.parse(commuter.get.body)
32
+ elsif !commuter.get.body.kind_of?(String)
33
+ commuter.get.body = template.render(commuter.get.body)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ require 'commute/core/commuter'
2
+
3
+ module Commute
4
+ module Common
5
+ module Conditional
6
+
7
+ class Tagged
8
+
9
+ def initialize processor, *tags
10
+ @processor = processor
11
+ @tags = tags
12
+ end
13
+
14
+ def call commuter
15
+ @processor.call unless @tags.any? { |tag| commuter.tagged? tag }
16
+ end
17
+ end
18
+
19
+ class NotTagged < Tagged
20
+
21
+ def call commuter
22
+ @processor.call commuter if @tags.all? { |tag| !commuter.tagged? tag }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ require "em-synchrony"
2
+ require "em-synchrony/em-http"
3
+
4
+ require 'commute/common/em_http_request_adapter'
5
+
6
+ module Commute
7
+ module Common
8
+
9
+ # Public: Adapter that uses em-synchrony (with em-http-request).
10
+ #
11
+ # Requires use of em-synchrony and thus Eventmachine.
12
+ #
13
+ class EmSynchronyAdapter < EmHttpRequestAdapter
14
+ @id = :adapter
15
+
16
+ # Internal: Make a request through em-synchrony.
17
+ def call commuter, options = {}
18
+ # Create a native em-http request.
19
+ em_request = to_request commuter.get
20
+
21
+ # Create a Commute response.
22
+ response = to_response em_request.response_header, em_request.response
23
+ response.request = commuter.get
24
+ # Set it on the commuter.
25
+ commuter.set response
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ require 'em-http-request'
2
+
3
+ require 'commute/core/response'
4
+
5
+ module Commute
6
+ module Common
7
+
8
+ # Public: Adapter that uses em-http-request, an Eventmachine based
9
+ # HTTP client, as a underlaying commute engine.
10
+ #
11
+ # This requires all the commute requests to be made within the
12
+ # Eventmachine loop.
13
+ #
14
+ class EmHttpRequestAdapter
15
+ @id = :adapter
16
+
17
+ # Internal: Make a request through Eventmachine.
18
+ def call commuter, options = {}
19
+ # Commuter delay for async processing.
20
+ commuter.delay do |&done|
21
+ # Create a native em-http request.
22
+ em_request = to_request commuter.get
23
+
24
+ # Set the callback.
25
+ em_request.callback do
26
+ # Create a Commute response.
27
+ response = to_response em_request.response_header, em_request.response
28
+ response.request = request
29
+ # Set it on the commuter.
30
+ commuter.set response
31
+ # Async call finished.
32
+ done.call
33
+ end
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # Internal: Converts a Commute requests into an em-http request.
40
+ def to_request request
41
+ EventMachine::HttpRequest.new(request.url).send request.method, \
42
+ query: request.query,
43
+ head: request.headers,
44
+ body: request.body
45
+ end
46
+
47
+ # Internal: Converts an em-http response into a Commute response.
48
+ # Note: trims whitespace bodies.
49
+ def to_response em_response_header, em_response
50
+ Commute::Response.new.tap do |response|
51
+ response.code = em_response_header.status
52
+ response.body = em_response unless em_response.strip.empty?
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ require 'multi_json'
2
+
3
+ module Commute
4
+ module Common
5
+
6
+ module Json
7
+ class Parse
8
+
9
+ OPTIONS = {
10
+ symbolize_keys: true
11
+ }
12
+
13
+ def call c, options = {}
14
+ response = c.get
15
+ response.body = MultiJson.load response.body, OPTIONS.merge(options)
16
+ end
17
+ end
18
+
19
+ class Render
20
+
21
+ def call c, options = {}
22
+ request = c.get
23
+ request.body = MultiJson.dump request.body
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ require 'typhoeus'
2
+
3
+ require 'commute/core/response'
4
+
5
+ module Commute
6
+ module Common
7
+
8
+ # Internal: Adapter that uses Typhoeus to make requests.
9
+ #
10
+ # Compatibility:
11
+ #
12
+ # TODO
13
+ #
14
+ class TyphoeusAdapter
15
+ @id = :adapter
16
+
17
+ def call commuter, options = {}
18
+ commuter.change do |request|
19
+ # puts request.inspect
20
+ # Build a Typhoeus request from this request.
21
+ typhoeus_request = Typhoeus::Request.new request.url, \
22
+ method: request.method,
23
+ body: request.body.to_s,
24
+ params: request.query || {},
25
+ headers: request.headers
26
+ # Run the request.
27
+ hydra = Typhoeus::Hydra.new
28
+ hydra.queue typhoeus_request
29
+ hydra.run
30
+ typhoeus_response = typhoeus_request.response
31
+ # Build a Commute response.
32
+ Response.new(request).tap do |response|
33
+ response.code = typhoeus_response.code
34
+ response.body = typhoeus_response.body
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module Commute
2
+ module Common
3
+
4
+ class Xml
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module Commute
2
+
3
+ class Configuration
4
+ class << self
5
+ attr_accessor :adapter
6
+ end
7
+ end
8
+ end