commute 0.1.2 → 0.2.0.rc.1

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.
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