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
@@ -0,0 +1,58 @@
1
+ require 'uri'
2
+
3
+ module Commute
4
+
5
+ class Request
6
+
7
+ # Scheme (http(s))
8
+ attr_accessor :scheme
9
+
10
+ # Host to connect to (String).
11
+ attr_accessor :host
12
+
13
+ # Port to connect to (Integer).
14
+ attr_accessor :port
15
+
16
+ # Path to request (String).
17
+ attr_accessor :path
18
+
19
+ # Url parameters (Hash).
20
+ attr_accessor :query
21
+ alias :params :query
22
+ alias :params= :query=
23
+
24
+ # The HTTP Method of the request (Symbol).
25
+ # Mostly :get, :post, :put, :patch, :delete
26
+ attr_accessor :method
27
+
28
+ # Request headers.
29
+ attr_accessor :headers
30
+
31
+ # The headers (Hash).
32
+
33
+ # The Body.
34
+ attr_accessor :body
35
+
36
+ def initialize
37
+ @headers = {}
38
+ @query = {}
39
+ end
40
+
41
+ def url= url
42
+ uri = URI url
43
+ @scheme = uri.scheme
44
+ @host = uri.host
45
+ @port = uri.port
46
+ @path = uri.path
47
+ @query = Hash[uri.query.split('&').map { |p| p.split('=') }] if uri.query
48
+ end
49
+
50
+ # SCHEME! TODO
51
+ def url
52
+ query = @query.map { |k,v| "#{k}=#{v}" }.join '&'
53
+ url = "#{@scheme}://#{@host}:#{@port}#{@path}"
54
+ url << "?#{query}" unless query.nil? || query.empty?
55
+ url
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ module Commute
2
+
3
+ class Response
4
+
5
+ # The request responsible for this response.
6
+ attr_accessor :request
7
+
8
+ # Response code.
9
+ attr_accessor :code
10
+
11
+ # The Body.
12
+ attr_accessor :body
13
+
14
+ def initialize request = nil
15
+ @request = request
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,180 @@
1
+ require 'commute/core/commuter'
2
+
3
+ module Commute
4
+
5
+ # Internal: A Sequence is an ordered list of processors, that all
6
+ # work together to incrementally process a Commuter.
7
+ #
8
+ class Sequence
9
+
10
+ # Public: Initializes a sequence without processors.
11
+ def initialize &alter
12
+ @processors = []
13
+ @index = {}
14
+ self.alter &alter if block_given?
15
+ end
16
+
17
+ # Public: Alters the sequence.
18
+ #
19
+ # Yields the sequence itself if the arity of the block is 1.
20
+ # Evaluates the block if the arity is 0.
21
+ #
22
+ # Returns the sequence itself.
23
+ def alter &alter
24
+ if alter.arity == 0
25
+ instance_eval &alter
26
+ else
27
+ yield self
28
+ end
29
+ # Return the Sequence.
30
+ return self
31
+ end
32
+
33
+ # Returns an duplicate collection of processors.
34
+ def processors
35
+ @processors.clone
36
+ end
37
+
38
+ # Internal: Processes a commuter with al processors in the sequence.
39
+ #
40
+ # When a commuter provides parameters for a processor through the
41
+ # parameters method, given the processor id. Then those parameters
42
+ # are passed as an argument to the call method of the processor (if applicable).
43
+ #
44
+ # Returns Nothing.
45
+ def call commuter, options = {}
46
+ call_one 0, commuter
47
+ end
48
+
49
+ def call_one index, commuter
50
+ # Get the processor using the index.
51
+ processor = @processors[index]
52
+ # Immediately return if no processor is found.
53
+ return nil unless processor
54
+ # Identify the processor.
55
+ # TODO: not very performant
56
+ processor_id = @index.key processor
57
+ # Immediately jumpt to the next processor if this one is disabled.
58
+ if commuter.enabled? processor_id
59
+ # Get the parameters.
60
+ parameters = commuter.parameters(processor_id) if processor_id
61
+ # Calling the processor.
62
+ if parameters
63
+ processor.call commuter, parameters
64
+ else
65
+ processor.call commuter
66
+ end
67
+ end
68
+ # Call self for the next processor.
69
+ call_next = Proc.new { call_one index + 1, commuter }
70
+ if commuter.delayed?
71
+ commuter.wait &call_next
72
+ else
73
+ call_next.call
74
+ end
75
+ end
76
+
77
+ # Public: Appends a processor to a the list of processors.
78
+ #
79
+ # options - Optional parameters for processor naming and placement (default: {}):
80
+ # :as - The name of the processor in the sequence (optional).
81
+ # :after - A reference processor id after which the processor needs to be appended.
82
+ #
83
+ # Returns the appended processor.
84
+ def append processor, options = {}
85
+ # When a reference is given, append after it.
86
+ if options[:after]
87
+ @processors.insert id_index(options[:after]) + 1, processor
88
+ else
89
+ @processors << processor
90
+ end
91
+ # Identify the processor using different methods.
92
+ id = identify processor, options[:as]
93
+ # Add the processor to the index for direct access purposes.
94
+ @index[id] = processor if id
95
+ # Return the appended processor.
96
+ processor
97
+ end
98
+
99
+ # TODO
100
+ def insert processor, options = {}
101
+ # When a reference is given, insert before it.
102
+ if options[:before]
103
+ @processors.insert id_index(options[:before]) - 1, processor
104
+ else
105
+ @processors.unshift processor
106
+ end
107
+ # Identify the processor using different methods.
108
+ id = identify processor, options[:as]
109
+ # Add the processor to the index for direct access purposes.
110
+ @index[id] = processor if id
111
+ # Return the appended processor.
112
+ processor
113
+ end
114
+
115
+ # TODO TEST
116
+ def replace processor_id
117
+ processor = at processor_id
118
+ raise "processor does not exist #{processor_id}" unless processor
119
+ replacement = yield processor
120
+ @processors[@processors.index processor] = replacement
121
+ @index[processor_id] = replacement
122
+ end
123
+
124
+ # Public: Removes a processor from the sequence.
125
+ #
126
+ # When a processor object is given, the processor is removed. When its
127
+ # Symbol identifier is given, the associated processor is removed.
128
+ #
129
+ # Returns the removed processor.
130
+ def remove processor
131
+ if processor.is_a?(Symbol)
132
+ processor_id = processor
133
+ processor = at(processor)
134
+ @processors.delete processor
135
+ @index.delete processor_id
136
+ else
137
+ # Remove from list of processors.
138
+ @processors.delete processor
139
+ # Remove from index (if necessary).
140
+ @index.delete_if { |id, pr| pr == processor}
141
+ end
142
+ # Return the deleted processor.
143
+ processor
144
+ end
145
+
146
+ # Public
147
+ # Returns the processor with the given id.
148
+ #
149
+ # id - The Symbol identifier of a processor.
150
+ def at id
151
+ @index[id]
152
+ end
153
+
154
+ # Internal: Clones the sequence.
155
+ def dup
156
+ Sequence.new.tap do |sequence|
157
+ sequence.processors = @processors.clone
158
+ sequence.index = @index.clone
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ def identify processor, override = nil
165
+ if override
166
+ override
167
+ else
168
+ processor.class.instance_variable_get :@id
169
+ end
170
+ end
171
+
172
+ def id_index id
173
+ @processors.index at(id)
174
+ end
175
+
176
+ protected
177
+
178
+ attr_writer :processors, :index
179
+ end
180
+ end
@@ -0,0 +1,145 @@
1
+ require 'commute/core/sequence'
2
+
3
+ module Commute
4
+
5
+ # Internal: A stack is a collection of sequences with
6
+ # A main sequence uses as a starting point for processing.
7
+ #
8
+ # When the stack is called with a processable, it gets
9
+ # processed by the main sequence. That main sequence
10
+ # can easily "embed" other sequences by adding them as
11
+ # a processer (a sequence is also a processor).
12
+ #
13
+ # Example:
14
+ #
15
+ # stack = Stack.new do |stack, sequence|
16
+ # stack.sequence(:calculation) do
17
+ # append Proc.new { |n| n.data += 1 }
18
+ # append Proc.new { |n| n.data *= 2 }
19
+ # end
20
+ #
21
+ # sequence.append stack.sequencer(:calculation)
22
+ # sequence.append Proc.new { |p| p.process { |n| "The result is #{n}" }}
23
+ # end
24
+ #
25
+ # stack.call Processable.new(context, 1)
26
+ # # => "The result is 4"
27
+ #
28
+ class Stack
29
+
30
+ # Public: Constructs a stack with a main sequence.
31
+ #
32
+ # sequence - Sequence used as main sequence.
33
+ #
34
+ # Yields
35
+ # 1. The Stack itself. (If the arity of the block is 2)
36
+ # 2. a new sequence to use a main sequence when no
37
+ # sequence is explicitely given.
38
+ #
39
+ # Raises an error if neither is given.
40
+ #
41
+ # Returns a Stack with a main sequence.
42
+ def initialize sequence = nil, &main
43
+ @sequences = {}
44
+ @main = if sequence
45
+ sequence
46
+ else
47
+ if main.arity == 1
48
+ Sequence.new &main
49
+ else
50
+ sequence = Sequence.new
51
+ yield self, sequence
52
+ sequence
53
+ end
54
+ end
55
+ end
56
+
57
+ # Public: Adds a sequence to the stack.
58
+ #
59
+ # sequence - The Sequence to be added.
60
+ # name - The name of the sequence.
61
+ #
62
+ # Returns the Sequence that was added.
63
+ def add sequence, name
64
+ @sequences[name] = sequence
65
+ end
66
+
67
+ # Public: Get a sequence from the stack.
68
+ #
69
+ # name - The name of the wanted Sequence.
70
+ #
71
+ # Returns the asked sequence.
72
+ def get name
73
+ @sequences[name]
74
+ end
75
+
76
+ # Public: Manipulate an either existing or new sequence.
77
+ #
78
+ # name - The name of the sequence to alter or create.
79
+ #
80
+ # Yields
81
+ # 1. The Stack itself. (If the arity of the block is 2)
82
+ # 2. The existing sequence with given name or
83
+ # adds a new yielded sequence with the name to the stack.
84
+ #
85
+ # Returns the Sequence.
86
+ def sequence name = nil, &alter
87
+ if name
88
+ @sequences[name] || add(Sequence.new(&alter), name)
89
+ else
90
+ @main
91
+ end
92
+ end
93
+
94
+ # Public: Creates a processor that lazily fetches a named sequence
95
+ # on a call.
96
+ #
97
+ # name - The name of the sequence this sequencer stands in for.
98
+ #
99
+ # Returns a Sequencer
100
+ def sequencer name = nil
101
+ if name
102
+ Sequencer.new.here(name)
103
+ else
104
+ Sequencer.new
105
+ end
106
+ end
107
+
108
+ # Internal: Calls the stack aka call the main sequence.
109
+ #
110
+ # processable - The thing to process.
111
+ #
112
+ # Returns the processable
113
+ def call processable
114
+ @main.call processable
115
+ end
116
+
117
+ # Public: Alters the stack.
118
+ #
119
+ # Yields the stack itself if the arity of the block is 1.
120
+ # Evaluates the block if the arity is 0.
121
+ #
122
+ # Returns the stack itself.
123
+ def alter &alter
124
+ if alter.arity == 0
125
+ instance_eval &alter
126
+ else
127
+ yield self, @main
128
+ end
129
+ # Return the Stack.
130
+ return self
131
+ end
132
+
133
+ # Internal: Clones the sequence.
134
+ def dup
135
+ Stack.new(@main.dup).tap do |stack|
136
+ stack.sequences = Hash[@sequences.map { |k,v| [k, v.dup] }]
137
+ end
138
+ end
139
+
140
+ protected
141
+
142
+ attr_writer :sequences
143
+ end
144
+ end
145
+
@@ -1,3 +1,3 @@
1
1
  module Commute
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0.rc.1"
3
3
  end
data/lib/commute.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require "commute/version"
2
2
 
3
- require 'commute/api'
3
+ require 'commute/configuration'
4
+
5
+ require 'logger'
6
+ require 'commute/core/api'
4
7
 
5
8
  module Commute
6
- # Your code goes here...
7
9
  end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'commute/aspects/caching'
3
+
4
+ describe Commute::Aspect::Caching do
5
+
6
+ class TestApi < Commute::Api
7
+ include Commute::Aspect::Caching
8
+ end
9
+
10
+ it '' do
11
+ end
12
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+ require 'commute/core/context'
3
+ require 'commute/core/request'
4
+ require 'commute/aspects/url'
5
+
6
+ describe Commute::Aspect::Url do
7
+
8
+ Klass = Class.new(Commute::Builder) do |klass|
9
+ include Commute::Aspect::Url::ClassMethods
10
+ end
11
+
12
+ let(:request) { Commute::Request.new }
13
+
14
+ it 'should create an url transformation without parameters' do
15
+ transformation = example('http://api.example.com')
16
+ # Hash that acts as a context.
17
+ context = {}
18
+ transformation.call(request, context)
19
+ # Checks.
20
+ request.scheme.must_equal 'http'
21
+ request.host.must_equal 'api.example.com'
22
+ request.path.must_be_empty
23
+ end
24
+
25
+ it 'should create an url transformation with parameters in the host and path' do
26
+ transformation = example('https://api.$root.com:3000/1/$scope/$id.xml')
27
+ # Hash that acts as a context.
28
+ context = {
29
+ root: 'example',
30
+ scope: 'people',
31
+ id: '3'
32
+ }
33
+ transformation.call(request, context)
34
+ # Checks.
35
+ request.scheme.must_equal 'https'
36
+ request.host.must_equal 'api.example.com'
37
+ request.path.must_equal '/1/people/3.xml'
38
+ request.port.must_equal '3000'
39
+ end
40
+
41
+ it 'should create an url transformation with parameters in the host and path when only some of the values are present' do
42
+ transformation = example('https://api.$root.com:3000/1/$scope/$id.xml')
43
+ # Hash that acts as a context.
44
+ context = {
45
+ root: 'example',
46
+ scope: 'people'
47
+ }
48
+ transformation.call(request, context)
49
+ # Checks.
50
+ request.scheme.must_equal 'https'
51
+ request.host.must_equal 'api.example.com'
52
+ request.path.must_equal '/1/people.xml'
53
+ request.port.must_equal '3000'
54
+ end
55
+
56
+ private
57
+
58
+ def example pattern
59
+ Klass.new(Commute::Context.new).url(pattern).transformations.first
60
+ end
61
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require 'commute/core/api'
3
+
4
+ describe Commute::Api do
5
+
6
+ class TestApi < Commute::Api
7
+ with(text: 'hello world')
8
+
9
+ with(id: 1)
10
+
11
+ using do
12
+ sequence.append Proc.new {}
13
+ end
14
+
15
+ def all
16
+ with(text: 'go', help: true).get
17
+ end
18
+ end
19
+
20
+ class TestInheritance < TestApi
21
+ with(id: 2)
22
+
23
+ using do
24
+ sequence.append Proc.new {}
25
+ end
26
+ end
27
+
28
+ let(:api_klass) { TestApi }
29
+
30
+ let(:api_klass2) do
31
+ Class.new(Commute::Api) do
32
+ with text: 'goodbye world'
33
+ end
34
+ end
35
+
36
+ it 'should allow us to define a base context' do
37
+ api_klass.context.parameters.must_equal id: 1, text: 'hello world'
38
+ end
39
+
40
+ it 'should isolate base contexts for each api' do
41
+ api_klass2.context.parameters.must_equal text: 'goodbye world'
42
+ end
43
+
44
+ it 'should allow us to modify the base context externally' do
45
+ build = api_klass.builder.with id: 2
46
+ api_klass.context.parameters[:id].must_equal 2
47
+ api_klass.builder.with id: 1
48
+ end
49
+
50
+ it 'should create a new Api instance when a method is called on the class' do
51
+ context = api_klass.with(id: 2).all.context
52
+ context.parameters.must_equal id: 2, text: 'go', help: true
53
+ end
54
+
55
+ it 'should deal with inheritance' do
56
+ context = TestInheritance.new.context
57
+ context.parameters[:id].must_equal 2
58
+ context.stack.sequence.processors.size.must_be :>=, 2
59
+ end
60
+
61
+ describe '#new' do
62
+ it 'should build on the base context without modifying it' do
63
+ api = api_klass.new.context
64
+ api.with(id: 2).parameters[:id].must_equal 2
65
+ context = api.with(text: 'hi!').all.context
66
+ context.parameters[:id].must_equal 1
67
+ context.with(id: 2).parameters.must_equal text: 'go', help: true, id: 2
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+ require 'commute/core/context'
3
+ require 'commute/core/request'
4
+
5
+ describe Commute::Builder do
6
+
7
+ let(:parameters) {{}}
8
+ let(:stack) { Commute::Stack.new { |stack, main|
9
+ main.append Proc.new {}
10
+ }}
11
+ let(:transformations) {[]}
12
+ let(:disables) {[]}
13
+
14
+ let(:context) { Commute::Context.new stack, parameters, transformations, disables }
15
+
16
+ let(:builder) { Commute::Builder.new context }
17
+
18
+ let(:request) { Commute::Request.new }
19
+
20
+ describe '#with' do
21
+ it 'should add parameters to the context' do
22
+ builder.with(id: 1, text: 'hello').parameters.must_equal \
23
+ id: 1, text: 'hello'
24
+ end
25
+
26
+ it 'should merge collisions by merging hashes and adding arrays' do
27
+ builder.
28
+ with(ids: [1,2], options: { limit: 1 }).
29
+ with(ids: [3,4], options: { index: 2 }).
30
+ parameters.must_equal \
31
+ ids: [1,2,3,4], options: { limit: 1, index: 2 }
32
+ end
33
+
34
+ describe 'with initial parameters' do
35
+ let(:parameters) {
36
+ { text: 'hello', limit: 10 }
37
+ }
38
+
39
+ it 'should add parameters to the context' do
40
+ builder.with(id: 1, text: 'world').parameters.must_equal \
41
+ id: 1, text: 'world', limit: 10
42
+ end
43
+ end
44
+
45
+ it 'should not include parameters with nil value' do
46
+ builder.with(id: nil).parameters.must_equal({})
47
+ end
48
+ end
49
+
50
+ describe '#without' do
51
+
52
+ it 'should remove parameters' do
53
+ builder.with(id: 1, text: 'hello').without(:id).parameters.must_equal text: 'hello'
54
+ end
55
+
56
+ it 'should work over contexts' do
57
+ context = builder.with(id: 1, text: 'hello').context
58
+ context2 = context.without(:id).context
59
+ context2.parameters.wont_include :id
60
+ context.parameters.must_include :id
61
+ end
62
+ end
63
+
64
+ describe '#enable' do
65
+ it 'should enable a processor' do
66
+ skip 'TODO'
67
+ end
68
+ end
69
+
70
+ describe '#disable' do
71
+ it 'should disable a processor' do
72
+ skip 'TODO'
73
+ end
74
+ end
75
+
76
+ describe '#transform' do
77
+ it 'should be able to add a static transform without dependencies' do
78
+ context = builder.transform { |request| request.method = :get }.context
79
+ context.transformations.first.call(request, context)
80
+ request.method.must_equal :get
81
+ end
82
+
83
+ it 'should be able to add a dynamic transform without dependencies' do
84
+ context = builder.with(id: 1).transform { |request, context|
85
+ request.path = "/#{context[:id]}"
86
+ }.context
87
+ context.transformations.first.call(request, context)
88
+ request.path.must_equal '/1'
89
+ end
90
+
91
+ it 'should be able to add a dynamic transform with dependencies' do
92
+ context = builder.with(id: 1).transform(:id) { |request, id|
93
+ request.path = "/#{id}"
94
+ }.context
95
+ context.transformations.first.call(request, context)
96
+ request.path.must_equal '/1'
97
+ end
98
+
99
+ it 'should never call the transformation when its dependencies are all nil' do
100
+ proc = Proc.new {}
101
+ proc.expects(:call).never
102
+ context = builder.transform(:id, &proc)
103
+ context.transformations.first.call(request, context)
104
+ end
105
+ end
106
+
107
+ describe '#using' do
108
+ it 'should work without a block' do
109
+ skip 'TODO'
110
+ end
111
+ end
112
+
113
+ describe '#context' do
114
+ it 'should be a snapshot of the builder' do
115
+ builder.with id: 1
116
+ context = builder.context
117
+ context.parameters.must_equal id: 1
118
+ builder = context.with text: 'hello world'
119
+ context.parameters.must_equal id: 1
120
+ builder.context.parameters.must_equal id: 1, text: 'hello world'
121
+ end
122
+ end
123
+ end