clientura 0.0.6

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.
@@ -0,0 +1,143 @@
1
+ require 'clientura/client/request'
2
+ require 'clientura/client/endpoint'
3
+ require 'clientura/client/middleware_function_context'
4
+
5
+ module Clientura
6
+ module Client
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ klass.include InstanceMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def registered_endpoints
14
+ @registered_endpoints ||= {}
15
+ end
16
+
17
+ def registered_pipes
18
+ @registered_pipes ||= {}
19
+ end
20
+
21
+ def registered_middleware
22
+ @registered_middleware ||= {}
23
+ end
24
+
25
+ [:get, :post, :put, :patch, :delete].each do |verb|
26
+ define_method verb do |name, path: nil|
27
+ register_endpoint(name, verb: verb, path: path || name.to_s)
28
+ end
29
+ end
30
+
31
+ def register_endpoint(name, verb:, path:)
32
+ registered_endpoints[name] = Endpoint.new verb,
33
+ path,
34
+ [*@middleware_context],
35
+ [*@pipes_context]
36
+
37
+ define_method name do |args = {}|
38
+ call_endpoint name, args
39
+ end
40
+
41
+ define_method "#{name}_promise" do |args = {}|
42
+ Concurrent::Promise.execute { send(name, args) }
43
+ end
44
+ end
45
+
46
+ def pipe(name, callable)
47
+ registered_pipes[name] = callable
48
+ end
49
+
50
+ def middleware(name, callable)
51
+ registered_middleware[name] = callable
52
+ end
53
+
54
+ def pipe_through(*pipes)
55
+ pipes = pipes.map { |o| normalize_mapper o }
56
+
57
+ @pipes_context ||= []
58
+ @pipes_context.push(*pipes)
59
+ yield
60
+ @pipes_context.pop pipes.count
61
+ end
62
+
63
+ def use_middleware(*middleware)
64
+ middleware = middleware.map { |o| normalize_mapper o }
65
+
66
+ @middleware_context ||= []
67
+ @middleware_context.push(*middleware)
68
+ yield
69
+ @middleware_context.pop middleware.count
70
+ end
71
+
72
+ def normalize_mapper(mapper)
73
+ case mapper
74
+ when Array
75
+ name, *config = mapper
76
+ { name: name, config: config }
77
+ else
78
+ { name: mapper, config: [] }
79
+ end
80
+ end
81
+ end
82
+
83
+ module InstanceMethods
84
+ def self.included(klass)
85
+ define_method :registered_endpoints do
86
+ klass.registered_endpoints
87
+ end
88
+
89
+ define_method :registered_pipes do
90
+ klass.registered_pipes
91
+ end
92
+
93
+ define_method :registered_middleware do
94
+ klass.registered_middleware
95
+ end
96
+ end
97
+
98
+ attr_writer :config
99
+
100
+ def config
101
+ @config ||= {}
102
+ end
103
+
104
+ def save_config(args)
105
+ self.config = config.merge args
106
+ end
107
+
108
+ def call_endpoint(name_, args)
109
+ endpoint = registered_endpoints.fetch(name_)
110
+
111
+ path = if endpoint.path.respond_to?(:call)
112
+ endpoint.path.call(args)
113
+ else
114
+ endpoint.path
115
+ end
116
+
117
+ middlewares = endpoint.middleware.map do |name:, config:|
118
+ { callable: registered_middleware.fetch(name), config: config }
119
+ end
120
+
121
+ request = middlewares
122
+ .reduce Request.new do |request_, callable:, config:|
123
+ middleware = MiddlewareFunctionContext.new(request: request_,
124
+ client: self,
125
+ args: args,
126
+ callable: callable,
127
+ config: config)
128
+ middleware.call
129
+ end
130
+
131
+ response = request.send(endpoint.verb, path)
132
+
133
+ endpoints = endpoint.pipes.map do |name:, config:|
134
+ -> (res) { registered_pipes.fetch(name).call(res, *config) }
135
+ end
136
+
137
+ endpoints.reduce response do |response_, pipe|
138
+ pipe.call response_
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ module Clientura
2
+ VERSION = File.read(File.expand_path('../../../VERSION/', __FILE__)).freeze
3
+ end
data/lib/clientura.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'http'
2
+ require 'concurrent'
3
+ require 'concurrent-edge'
4
+ require 'active_support/core_ext/object'
5
+ require 'active_support/json'
6
+
7
+ require 'clientura/version'
8
+ require 'clientura/client'
9
+
10
+ module Clientura
11
+ end
@@ -0,0 +1,204 @@
1
+ describe 'Ability to use this for solving real world problems' do
2
+ # Purpose of this spec is to define some real world use cases in a clean and
3
+ # readable manner to serve as an example of usage.
4
+
5
+ subject { instance }
6
+
7
+ let(:instance) { client_class.new(client_config) }
8
+ let(:client_config) { { uri: 'http://localhost:3001/real_world/' } }
9
+ let(:client_class) do
10
+ Class.new do
11
+ include Clientura::Client
12
+
13
+ middleware :configurable_uri, -> { update(:uri) { client.config[:uri] } }
14
+ middleware :header_with_token, -> { headers AuthToken: client.config[:token] }
15
+ middleware :pass_all_as_query_string, -> { update(:params) { args } }
16
+ middleware :namespace, -> (namespace) { update(:uri) { |uri| URI.join uri, namespace } }
17
+ middleware :bad_middleware, -> { raise 'Some Exception' }
18
+ middleware :slow_middleware, -> { sleep 0.1; self }
19
+
20
+ pipe :body_retriever, -> (res) { res.body.to_s }
21
+ pipe :parser, -> (res, parser) { parser.parse res }
22
+ pipe :answer_header, -> (res) { res.headers['Answer'] }
23
+ pipe :data_retriever, -> (res) { res['data'] }
24
+
25
+ use_middleware :configurable_uri do
26
+ get :root, path: '/'
27
+
28
+ use_middleware :header_with_token do
29
+ get :pass_token
30
+ end
31
+
32
+ pipe_through :body_retriever do
33
+ get :get_some, path: 'some'
34
+ put :put_some, path: 'some'
35
+ patch :patch_some, path: 'some'
36
+ post :post_some, path: 'some'
37
+ delete :delete_some, path: 'some'
38
+
39
+ pipe_through [:parser, JSON] do
40
+ get :parse_response
41
+
42
+ pipe_through :data_retriever do
43
+ get :comments, path: -> (params) { "comments/#{params[:id]}" }
44
+ get :users, path: -> (params) { "users/#{params[:id]}" }
45
+ get :attachments, path: -> (params) { "attachments/#{params[:id]}" }
46
+ end
47
+ end
48
+
49
+ use_middleware :pass_all_as_query_string do
50
+ get :pass_query_string
51
+ end
52
+ end
53
+
54
+ use_middleware [:namespace, 'namespace/'] do
55
+ get :namespaced
56
+ end
57
+
58
+ pipe_through :answer_header do
59
+ get :get_answer_header
60
+ end
61
+ end
62
+
63
+ use_middleware :bad_middleware do
64
+ get :raise_some_exception
65
+ end
66
+
67
+ use_middleware :slow_middleware, :bad_middleware do
68
+ get :slowly_raise_some_exception
69
+ end
70
+
71
+ def initialize(uri:, token: nil)
72
+ save_config uri: uri, token: token
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'My wish to see this at least working' do
78
+ subject { super().root.status }
79
+
80
+ it { should eq 200 }
81
+ end
82
+
83
+ describe 'My desire to use different verbs' do
84
+ describe 'GET' do
85
+ subject { super().get_some }
86
+
87
+ it { should eq 'get' }
88
+ end
89
+
90
+ describe 'POST' do
91
+ subject { super().post_some }
92
+
93
+ it { should eq 'post' }
94
+ end
95
+
96
+ describe 'PUT' do
97
+ subject { super().put_some }
98
+
99
+ it { should eq 'put' }
100
+ end
101
+
102
+ describe 'PATCH' do
103
+ subject { super().patch_some }
104
+
105
+ it { should eq 'patch' }
106
+ end
107
+
108
+ describe 'DELETE' do
109
+ subject { super().delete_some }
110
+
111
+ it { should eq 'delete' }
112
+ end
113
+ end
114
+
115
+ describe 'My desire to see some exceptions' do
116
+ subject { -> { instance.raise_some_exception } }
117
+
118
+ it { should raise_error 'Some Exception' }
119
+ end
120
+
121
+ describe 'My desire to work with promise' do
122
+ context 'when no errors raised' do
123
+ subject { super().root_promise }
124
+
125
+ it { should be_pending }
126
+
127
+ it 'has correct value' do
128
+ expect(subject.value.status).to eq 200
129
+ end
130
+
131
+ context 'when awaited' do
132
+ before { subject.value }
133
+
134
+ it 'fulfills' do
135
+ expect(subject).to be_fulfilled
136
+ end
137
+ end
138
+ end
139
+
140
+ context 'when error raised' do
141
+ subject { super().slowly_raise_some_exception_promise }
142
+
143
+ it { should be_pending }
144
+
145
+ it 'has nil value' do
146
+ expect(subject.value).to be nil
147
+ end
148
+
149
+ context 'when awaited' do
150
+ before { subject.value }
151
+
152
+ it 'rejects' do
153
+ expect(subject).to be_rejected
154
+ end
155
+
156
+ it 'has correct message' do
157
+ expect(subject.reason.message).to eq 'Some Exception'
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'My desire to pass some token in header' do
164
+ subject { super().pass_token.status }
165
+
166
+ let(:client_config) { super().merge token: token }
167
+
168
+ context 'when token is bad' do
169
+ let(:token) { 'Bad token!' }
170
+
171
+ it { should eq 403 }
172
+ end
173
+
174
+ context 'when token is good' do
175
+ let(:token) { 'Secret' }
176
+
177
+ it { should eq 200 }
178
+ end
179
+ end
180
+
181
+ describe 'My desire to parse response' do
182
+ subject { super().parse_response }
183
+
184
+ it { should eq 'data' => 'Awesome!' }
185
+ end
186
+
187
+ describe 'My desire to pass query string' do
188
+ subject { super().pass_query_string echo: 'Awesome!' }
189
+
190
+ it { should eq 'Awesome!' }
191
+ end
192
+
193
+ describe 'My desire to get response header' do
194
+ subject { super().get_answer_header }
195
+
196
+ it { should eq 'Awesome!' }
197
+ end
198
+
199
+ describe 'My desire to namespace routes' do
200
+ subject { super().namespaced.status }
201
+
202
+ it { should eq 200 }
203
+ end
204
+ end
@@ -0,0 +1,175 @@
1
+ describe Clientura::Client do
2
+ subject { client }
3
+
4
+ let(:uri) { 'http://localhost:3001' }
5
+ let(:client) { klass.new(uri: uri, token: 'InitializationToken') }
6
+ let(:klass) do
7
+ Class.new do
8
+ include Clientura::Client
9
+
10
+ middleware :static_token, -> (token) { headers(Token: token) }
11
+ middleware :init_token, -> { headers(token: client.config[:token]) }
12
+ middleware :token_passer, -> { headers(token: args[:token]) }
13
+ middleware :send_as_json, -> { update(:json) { args } }
14
+ middleware :pass_as_query, -> { update(:params) { |p| p.merge args } }
15
+ middleware :configurable_uri, lambda {
16
+ update(:uri) { |uri| URI.join client.config[:uri], uri }
17
+ }
18
+
19
+ pipe :body_retriever, -> (res) { res.body.to_s }
20
+ pipe :parser, -> (res, parser) { parser.parse res }
21
+ pipe :data_retriever, -> (res) { res['data'] }
22
+
23
+ use_middleware :configurable_uri do
24
+ pipe_through :body_retriever do
25
+ get :resource_raw, path: 'res'
26
+
27
+ pipe_through [:parser, JSON] do
28
+ get :root, path: '/'
29
+ get :resource, path: 'res'
30
+ get :name
31
+ get :echo_argument, path: -> (params) { "echo_argument/#{params[:argument]}" }
32
+
33
+ use_middleware :pass_as_query do
34
+ get :echo_param
35
+ end
36
+ end
37
+ end
38
+
39
+ pipe_through :body_retriever, [:parser, JSON] do
40
+ use_middleware [:static_token, 'StaticToken'] do
41
+ get :try_static_token, path: 'echo_header'
42
+ end
43
+
44
+ use_middleware [:static_token, 'Token'] do
45
+ get :echo_header_const, path: 'echo_header'
46
+ end
47
+
48
+ use_middleware :token_passer do
49
+ get :try_token_passer, path: 'echo_header'
50
+ get :echo_header
51
+ end
52
+
53
+ use_middleware :init_token do
54
+ get :try_init_token, path: 'echo_header'
55
+ end
56
+
57
+ get :configurable_uri_resource, path: 'res'
58
+
59
+ use_middleware :send_as_json do
60
+ post :send_json, path: 'data'
61
+ end
62
+ end
63
+ end
64
+
65
+ use_middleware :configurable_uri do
66
+ pipe_through :body_retriever, [:parser, JSON], :data_retriever do
67
+ use_middleware :send_as_json do
68
+ post :sum
69
+ end
70
+
71
+ get :comments, path: -> (params) { "comments/#{params[:id]}" }
72
+ get :users, path: -> (params) { "users/#{params[:id]}" }
73
+ get :tags, path: -> (params) { "tags/#{params[:id]}" }
74
+
75
+ use_middleware :pass_as_query do
76
+ get :left_operand
77
+ get :right_operand
78
+ end
79
+ end
80
+ end
81
+
82
+ def initialize(uri:, token:)
83
+ save_config uri: URI.parse(uri), token: token
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#root' do
89
+ subject { client.root }
90
+
91
+ it { should eq 'data' => 'root' }
92
+ end
93
+
94
+ describe '#resource' do
95
+ subject { client.resource }
96
+
97
+ it { should eq 'data' => 'resource' }
98
+ end
99
+
100
+ describe '#name' do
101
+ subject { client.name }
102
+
103
+ it { should eq 'data' => 'name' }
104
+ end
105
+
106
+ describe '#echo_argument' do
107
+ subject { client.echo_argument argument: 'Argument' }
108
+
109
+ it { should eq 'data' => 'Argument' }
110
+ end
111
+
112
+ describe '#echo_param' do
113
+ subject { client.echo_param param: 'Param' }
114
+
115
+ it { should eq 'data' => 'Param' }
116
+ end
117
+
118
+ describe '#echo_header' do
119
+ subject { client.echo_header token: 'Token' }
120
+
121
+ it { should eq 'data' => 'Token' }
122
+ end
123
+
124
+ describe '#echo_header_const' do
125
+ subject { client.echo_header_const }
126
+
127
+ it { should eq 'data' => 'Token' }
128
+ end
129
+
130
+ describe '#resource_raw' do
131
+ subject { client.resource_raw }
132
+
133
+ it { should eq JSON.dump data: 'resource' }
134
+ end
135
+
136
+ describe 'try_static_token' do
137
+ subject { client.try_static_token }
138
+
139
+ it { should eq 'data' => 'StaticToken' }
140
+ end
141
+
142
+ describe 'try_token_passer' do
143
+ subject { client.try_token_passer token: 'PassedToken' }
144
+
145
+ it { should eq 'data' => 'PassedToken' }
146
+ end
147
+
148
+ describe 'try_init_token' do
149
+ subject { client.try_init_token }
150
+
151
+ it { should eq 'data' => 'InitializationToken' }
152
+ end
153
+
154
+ describe 'configurable_uri_resource' do
155
+ subject { client.configurable_uri_resource }
156
+
157
+ it { should eq 'data' => 'resource' }
158
+ end
159
+
160
+ describe '#send_json' do
161
+ subject { client.send_json json_data: { foo: :bar } }
162
+
163
+ it { should eq 'data' => { 'foo' => 'bar' } }
164
+ end
165
+
166
+ describe '#sum' do
167
+ subject { client.sum sum: sum }
168
+
169
+ let(:sum) { client.left_operand(key: 'Secret') + client.right_operand(key: 'Secret') }
170
+
171
+ it { should eq true }
172
+ end
173
+
174
+ # it('__run_server__', :focus) { binding.pry }
175
+ end
@@ -0,0 +1,9 @@
1
+ # require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Clientura do
4
+ describe Clientura::VERSION do
5
+ subject { described_class }
6
+
7
+ it { should_not be_blank }
8
+ end
9
+ end
@@ -0,0 +1,64 @@
1
+ require 'simplecov'
2
+ require 'codeclimate-test-reporter'
3
+
4
+ module SimpleCov::Configuration
5
+ def clean_filters
6
+ @filters = []
7
+ end
8
+ end
9
+
10
+ SimpleCov.configure do
11
+ clean_filters
12
+ load_profile 'test_frameworks'
13
+ end
14
+
15
+ ENV['COVERAGE'] && SimpleCov.start do
16
+ add_filter '/.rvm/'
17
+ end
18
+
19
+ CodeClimate::TestReporter.start
20
+
21
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
22
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
23
+
24
+ require 'bundler'
25
+ Bundler.require(:test)
26
+ require 'clientura'
27
+
28
+ # Requires supporting files with custom matchers and macros, etc,
29
+ # in ./support/ and its subdirectories.
30
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
31
+
32
+ RSpec.configure do |config|
33
+ # config.expose_current_running_example_as :example
34
+ config.raise_errors_for_deprecations!
35
+ config.run_all_when_everything_filtered = true
36
+ config.filter_run focus: true
37
+ # config.mock_with :rspec
38
+ # config.use_transactional_fixtures = false
39
+
40
+ config.order = :rand
41
+
42
+ config.before(:suite) do |_example|
43
+ pid = Process.fork do
44
+ Rack::Handler::Thin.run TestServer, Port: 3001
45
+ # trap(:INT) { Rack::Handler::WEBrick.shutdown }
46
+ # Rack::Handler::WEBrick.run TestServer.new,
47
+ # Port: 3001,
48
+ # Logger: WEBrick::Log.new('/dev/null'),
49
+ # AccessLog: []
50
+ exit
51
+ end
52
+
53
+ at_exit do
54
+ Process.kill('INT', pid)
55
+ begin
56
+ Process.wait(pid)
57
+ rescue Errno::ECHILD
58
+ # ignore this error...I think it means the child process has already exited.
59
+ end
60
+ end
61
+
62
+ sleep 1
63
+ end
64
+ end