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