ocean-rails 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +72 -0
  4. data/Rakefile +38 -0
  5. data/lib/generators/ocean_scaffold/USAGE +8 -0
  6. data/lib/generators/ocean_scaffold/ocean_scaffold_generator.rb +76 -0
  7. data/lib/generators/ocean_scaffold/templates/controller_specs/create_spec.rb +71 -0
  8. data/lib/generators/ocean_scaffold/templates/controller_specs/delete_spec.rb +47 -0
  9. data/lib/generators/ocean_scaffold/templates/controller_specs/index_spec.rb +45 -0
  10. data/lib/generators/ocean_scaffold/templates/controller_specs/show_spec.rb +43 -0
  11. data/lib/generators/ocean_scaffold/templates/controller_specs/update_spec.rb +85 -0
  12. data/lib/generators/ocean_scaffold/templates/model_spec.rb +76 -0
  13. data/lib/generators/ocean_scaffold/templates/resource_routing_spec.rb +27 -0
  14. data/lib/generators/ocean_scaffold/templates/view_specs/_resource_spec.rb +55 -0
  15. data/lib/generators/ocean_scaffold/templates/views/_resource.json.jbuilder +8 -0
  16. data/lib/generators/ocean_setup/USAGE +8 -0
  17. data/lib/generators/ocean_setup/ocean_setup_generator.rb +93 -0
  18. data/lib/generators/ocean_setup/templates/Gemfile +19 -0
  19. data/lib/generators/ocean_setup/templates/alive_controller.rb +18 -0
  20. data/lib/generators/ocean_setup/templates/alive_routing_spec.rb +11 -0
  21. data/lib/generators/ocean_setup/templates/alive_spec.rb +12 -0
  22. data/lib/generators/ocean_setup/templates/api_constants.rb +19 -0
  23. data/lib/generators/ocean_setup/templates/application_controller.rb +8 -0
  24. data/lib/generators/ocean_setup/templates/application_helper.rb +34 -0
  25. data/lib/generators/ocean_setup/templates/config.yml.example +57 -0
  26. data/lib/generators/ocean_setup/templates/errors_controller.rb +14 -0
  27. data/lib/generators/ocean_setup/templates/gitignore +37 -0
  28. data/lib/generators/ocean_setup/templates/hyperlinks.rb +22 -0
  29. data/lib/generators/ocean_setup/templates/ocean_constants.rb +36 -0
  30. data/lib/generators/ocean_setup/templates/routes.rb +8 -0
  31. data/lib/generators/ocean_setup/templates/spec_helper.rb +47 -0
  32. data/lib/generators/ocean_setup/templates/zeromq_logger.rb +15 -0
  33. data/lib/ocean-rails.rb +38 -0
  34. data/lib/ocean/api.rb +263 -0
  35. data/lib/ocean/api_resource.rb +135 -0
  36. data/lib/ocean/flooding.rb +29 -0
  37. data/lib/ocean/ocean_application_controller.rb +214 -0
  38. data/lib/ocean/ocean_resource_controller.rb +76 -0
  39. data/lib/ocean/ocean_resource_model.rb +61 -0
  40. data/lib/ocean/selective_rack_logger.rb +33 -0
  41. data/lib/ocean/version.rb +3 -0
  42. data/lib/ocean/zero_log.rb +184 -0
  43. data/lib/ocean/zeromq_logger.rb +42 -0
  44. data/lib/tasks/ocean_tasks.rake +4 -0
  45. data/lib/template.rb +31 -0
  46. data/lib/templates/rails/scaffold_controller/controller.rb +91 -0
  47. metadata +267 -0
@@ -0,0 +1,14 @@
1
+ class ErrorsController < ApplicationController
2
+
3
+ skip_before_action :require_x_api_token
4
+ skip_before_action :authorize_action
5
+
6
+
7
+ def show
8
+ @exception = env['action_dispatch.exception']
9
+ @status_code = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
10
+ #@rescue_response = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
11
+ render_api_error @status_code, @exception.message
12
+ end
13
+
14
+ end
@@ -0,0 +1,37 @@
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore bundler config
8
+ /.bundle
9
+
10
+ # Ignore the default SQLite database.
11
+ /db/*.sqlite3
12
+
13
+ # Ignore all logfiles and tempfiles.
14
+ /log/*.log
15
+ /tmp
16
+ *.log
17
+
18
+ # Mac OS X
19
+ .DS_Store
20
+
21
+ # Ignore backup files
22
+ *~
23
+
24
+ # Ignore RubyMine local workspace settings
25
+ /.idea/workspace.xml
26
+
27
+ # Ignore any SQLite files
28
+ /db/*.sqlite3
29
+
30
+ # Ignore coverage artefacts
31
+ /coverage
32
+
33
+ # Ignore the tailored config.yml file
34
+ config/config.yml
35
+
36
+ # Ignore the Tork config dir
37
+ /.tork
@@ -0,0 +1,22 @@
1
+ RSpec::Matchers.define :be_hyperlinked do |link, regex, type="application/json"|
2
+
3
+ match do |hyperlinks|
4
+ (@h = hyperlinks[link]) &&
5
+ (@h['href'] =~ regex) &&
6
+ (@h['type'] == type)
7
+ end
8
+
9
+
10
+ failure_message_for_should do |actual|
11
+ "expected the resource representation to " + description
12
+ end
13
+
14
+ description do
15
+ result = "have a '#{link}' hyperlink"
16
+ result += " containing '#{regex.source}' in the URI" unless @h['href'] =~ regex
17
+ result += " leading to a resource of type '#{type}' (was '#{@h['type']}')" unless @h['type'] == type
18
+ result
19
+ end
20
+
21
+
22
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+ # This file will be replaced by an auto-generated one in deployment.
3
+ # YOU SHOULD NEVER CHANGE THE CONTENTS OF THIS FILE.
4
+ #
5
+ # Backend developers should never need to override the value here.
6
+ # The reason for this is that when developing services locally,
7
+ # their tests run in isolation with all external calls mocked away,
8
+ # and thus it doesn't matter what URLs a service generates when
9
+ # running tests locally on a developer's machine.
10
+ #
11
+ # If you're a frontend developer, however, the point of your testing
12
+ # is to exercise the entire SOA and thus you need access to a
13
+ # complete and fully functional system (which might or might not
14
+ # make calls to partners' systems, such as for hotel bookings).
15
+ #
16
+ # Thus, if you are a frontend developer, you override the string
17
+ # constant here to reflect the Chef environment (master, staging)
18
+ # you wish to run your local tests against by defining the environment
19
+ # variable OVERRIDE_OCEAN_API_HOST.
20
+ #
21
+ # When TeamCity runs its tests, it will set these constants to values
22
+ # appropriate for the Chef environment for which the tests are run.
23
+ # Thus, TeamCity will always run master branch frontend tests against
24
+ # the master Chef environment. However, you can run a personal build
25
+ # and specify the OCEAN_API_HOST value as an environment param in the
26
+ # build dialog.
27
+ #
28
+
29
+ OCEAN_API_HOST = (Rails.env == 'test' && "forbidden.#{BASE_DOMAIN}") ||
30
+ (ENV['OVERRIDE_OCEAN_API_HOST'] ||
31
+ ENV['OCEAN_API_HOST'] ||
32
+ "master-api.#{BASE_DOMAIN}").sub("<default>", "master")
33
+
34
+ OCEAN_API_URL = "https://#{OCEAN_API_HOST}"
35
+
36
+ INTERNAL_OCEAN_API_URL = OCEAN_API_URL.sub("https", "http").sub("api.", "lb.")
@@ -0,0 +1,8 @@
1
+ <%= class_name %>::Application.routes.draw do
2
+
3
+ scope "v1" do
4
+ # Put resource routes here
5
+
6
+ end
7
+
8
+ end
@@ -0,0 +1,47 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter "/vendor/"
4
+ end
5
+
6
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
7
+ ENV["RAILS_ENV"] ||= 'test'
8
+ require File.expand_path("../../config/environment", __FILE__)
9
+ require 'rspec/rails'
10
+ require 'rspec/autorun'
11
+
12
+ # Requires supporting ruby files with custom matchers and macros, etc,
13
+ # in spec/support/ and its subdirectories.
14
+ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
15
+
16
+ RSpec.configure do |config|
17
+ # ## Mock Framework
18
+ #
19
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
20
+ #
21
+ # config.mock_with :mocha
22
+ # config.mock_with :flexmock
23
+ # config.mock_with :rr
24
+
25
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
26
+ #config.fixture_path = "#{::Rails.root}/spec/fixtures"
27
+
28
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
29
+ # examples within a transaction, remove the following line or assign false
30
+ # instead of true.
31
+ config.use_transactional_fixtures = true
32
+
33
+ # If true, the base class of anonymous controllers will be inferred
34
+ # automatically. This will be the default behavior in future versions of
35
+ # rspec-rails.
36
+ config.infer_base_class_for_anonymous_controllers = false
37
+
38
+ # Run specs in random order to surface order dependencies. If you find an
39
+ # order dependency and want to debug it, you can fix the order by providing
40
+ # the seed, which is printed after each run.
41
+ # --seed 1234
42
+ config.order = "random"
43
+
44
+ # Make "FactoryGirl" superfluous
45
+ config.include FactoryGirl::Syntax::Methods
46
+ end
47
+
@@ -0,0 +1,15 @@
1
+
2
+ if Rails.env == 'production'
3
+
4
+ # Use a different logger for distributed setups
5
+ Rails.logger = ActiveSupport::TaggedLogging.new(ZeromqLogger.new)
6
+ Rails.logger.level = Logger::INFO
7
+ Rails.logger.log_tags = []
8
+
9
+ # Announce us
10
+ Rails.logger.info "Initialising"
11
+
12
+ # Make sure we log our exit
13
+ at_exit { Rails.logger.info "Exiting" }
14
+
15
+ end
@@ -0,0 +1,38 @@
1
+ require "ocean/api"
2
+ require "ocean/api_resource"
3
+ require "ocean/ocean_resource_model" if defined? ActiveRecord
4
+ require "ocean/ocean_resource_controller" if defined? ActionController
5
+ require "ocean/ocean_application_controller"
6
+ require "ocean/zero_log"
7
+ require "ocean/zeromq_logger"
8
+ require "ocean/selective_rack_logger"
9
+ require "ocean/flooding"
10
+
11
+ INVALIDATE_MEMBER_DEFAULT = ["($|/|\\?)"]
12
+ INVALIDATE_COLLECTION_DEFAULT = ["($|\\?)"]
13
+
14
+ module Ocean
15
+ class Railtie < Rails::Railtie
16
+ # Silence the /alive action
17
+ initializer "ocean.swap_logging_middleware" do |app|
18
+ app.middleware.swap Rails::Rack::Logger, SelectiveRackLogger
19
+ end
20
+ # Make sure the generators use the gem's templates first
21
+ config.app_generators do |g|
22
+ g.templates.unshift File::expand_path('../templates', __FILE__)
23
+ end
24
+ end
25
+ end
26
+
27
+
28
+ #
29
+ # For stubbing authorisation calls in RSpec
30
+ #
31
+ def permit_with(status, user_id: 123, creator_uri: "https://api.example.com/v1/api_users/#{user_id}")
32
+ Api.stub(:permitted?).
33
+ and_return(double(:status => status,
34
+ :body => {'authentication' =>
35
+ {'user_id' => user_id,
36
+ '_links' => { 'creator' => {'href' => creator_uri,
37
+ 'type' => 'application/json'}}}}))
38
+ end
data/lib/ocean/api.rb ADDED
@@ -0,0 +1,263 @@
1
+ #
2
+ # We need to monkey-patch Faraday to pull off PURGE and BAN
3
+ #
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+
7
+ module Faraday #:nodoc: all
8
+ class Connection
9
+
10
+ METHODS << :purge
11
+ METHODS << :ban
12
+
13
+ # purge/ban(url, params, headers)
14
+ %w[purge ban].each do |method|
15
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
+ def #{method}(url = nil, params = nil, headers = nil)
17
+ run_request(:#{method}, url, nil, headers) { |request|
18
+ request.params.update(params) if params
19
+ yield request if block_given?
20
+ }
21
+ end
22
+ RUBY
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+
29
+ #
30
+ # This class encapsulates all logic for calling other API services.
31
+ #
32
+ class Api
33
+
34
+ #
35
+ # When given a symbol or string naming a resource, returns a string
36
+ # such as +v1+ naming the latest version for the resource.
37
+ #
38
+ def self.version_for(resource_name)
39
+ API_VERSIONS[resource_name.to_s] || API_VERSIONS['_default']
40
+ end
41
+
42
+ #
43
+ # Given that this service has authenticated successfully with the Auth service,
44
+ # returns the token returned as part of the authentication response.
45
+ #
46
+ def self.token
47
+ @token
48
+ end
49
+
50
+
51
+ #
52
+ # Makes a HTTP request to +host_url+ using the HTTP method +method+. The +resource_name+
53
+ # is used to obtain the latest version string of the resource. The arg +path+ is the
54
+ # local path, +args+ is a hash of query args, and +headers+ a hash of extra HTTP headers.
55
+ #
56
+ # Returns the response in its entirety so that the caller can examine its status and body.
57
+ #
58
+ def self.call(host_url, http_method, resource_name, path, args={}, headers={})
59
+ # Set up the connection parameters
60
+ conn = Faraday.new(host_url) do |c|
61
+ c.response :json, :content_type => /\bjson$/ # Convert the response body to JSON
62
+ c.adapter Faraday.default_adapter # Use net-http
63
+ end
64
+ api_version = version_for resource_name
65
+ path = "/#{api_version}#{path}"
66
+ # Make the call. TODO: retries?
67
+ response = conn.send(http_method, path, args, headers) do |request|
68
+ request.headers['Accept'] = 'application/json'
69
+ request.headers['Content-Type'] = 'application/json'
70
+ end
71
+ response
72
+ end
73
+
74
+ #
75
+ # Convenience method to make an internal GET request to the Ocean Api. The +resource_name+
76
+ # is used to obtain the latest version string of the resource. The arg +path+ is the
77
+ # local path, +args+ is a hash of query args, and +headers+ a hash of extra HTTP headers.
78
+ #
79
+ # Returns the response in its entirety so that the caller can examine its status and body.
80
+ #
81
+ #
82
+ def self.get(*args) call(INTERNAL_OCEAN_API_URL, :get, *args); end
83
+
84
+ #
85
+ # Convenience method to make an internal POST request to the Ocean Api. The +resource_name+
86
+ # is used to obtain the latest version string of the resource. The arg +path+ is the
87
+ # local path, +args+ is a hash of query args, and +headers+ a hash of extra HTTP headers.
88
+ #
89
+ # Returns the response in its entirety so that the caller can examine its status and body.
90
+ #
91
+ #
92
+ def self.post(*args) call(INTERNAL_OCEAN_API_URL, :post, *args); end
93
+
94
+ #
95
+ # Convenience method to make an internal PUT request to the Ocean Api. The +resource_name+
96
+ # is used to obtain the latest version string of the resource. The arg +path+ is the
97
+ # local path, +args+ is a hash of query args, and +headers+ a hash of extra HTTP headers.
98
+ #
99
+ # Returns the response in its entirety so that the caller can examine its status and body.
100
+ #
101
+ #
102
+ def self.put(*args) call(INTERNAL_OCEAN_API_URL, :put, *args); end
103
+
104
+ #
105
+ # Convenience method to make an internal DELETE request to the Ocean Api. The +resource_name+
106
+ # is used to obtain the latest version string of the resource. The arg +path+ is the
107
+ # local path, +args+ is a hash of query args, and +headers+ a hash of extra HTTP headers.
108
+ #
109
+ # Returns the response in its entirety so that the caller can examine its status and body.
110
+ #
111
+ #
112
+ def self.delete(*args) call(INTERNAL_OCEAN_API_URL, :delete, *args); end
113
+
114
+
115
+ #
116
+ # Like Api.call, but makes the requests in parallel. (Parallel calls not implemented yet.)
117
+ #
118
+ def self.call_p(url, http_method, path, args={}, headers={})
119
+ conn = Faraday.new(url) do |c|
120
+ c.adapter Faraday.default_adapter # Use net-http
121
+ end
122
+ conn.send(http_method, path, args, headers)
123
+ end
124
+
125
+ #
126
+ # Makes an internal PURGE call to all Varnish instances. The call is made in parallel.
127
+ # Varnish will only accept PURGE requests coming from the local network.
128
+ #
129
+ def self.purge(*args)
130
+ LOAD_BALANCERS.each do |host|
131
+ call_p("http://#{host}", :purge, *args)
132
+ end
133
+ end
134
+
135
+ #
136
+ # Makes an internal BAN call to all Varnish instances. The call is made in parallel.
137
+ # Varnish will only accept PURGE requests coming from the local network.
138
+ #
139
+ def self.ban(path)
140
+ LOAD_BALANCERS.each do |host|
141
+ call_p("http://#{host}", :ban, path)
142
+ end
143
+ end
144
+
145
+
146
+ #
147
+ # Authenticates against the Auth service (which must be deployed and running) with
148
+ # a given +username+ and +password+. If successful, the authentication token is returned. The
149
+ # token is also assigned to the instance variable @token. If not successful, +nil+ is returned.
150
+ #
151
+ def self.authenticate(username=API_USER, password=API_PASSWORD)
152
+ response = Api.post(:auth, "/authentications", nil,
153
+ {'X-API-Authenticate' => encode_credentials(username, password)})
154
+ case response.status
155
+ when 201
156
+ @token = response.body['authentication']['token']
157
+ when 400
158
+ # Malformed credentials. Don't repeat the request.
159
+ nil
160
+ when 403
161
+ # Does not authenticate. Don't repeat the request.
162
+ nil
163
+ when 500
164
+ # Error. Don't repeat.
165
+ nil
166
+ else
167
+ # Should never end up here.
168
+ raise "Authentication weirdness"
169
+ end
170
+ end
171
+
172
+
173
+ #
174
+ # Encodes a username and password for authentication in the format used for standard HTTP
175
+ # authentication. The encoding can be reversed and is intended only to lightly mask the
176
+ # credentials so that they're not immediately apparent when reading logs.
177
+ #
178
+ def self.encode_credentials(username, password)
179
+ ::Base64.strict_encode64 "#{username}:#{password}"
180
+ end
181
+
182
+ #
183
+ # Takes encoded credentials (e.g. by Api.encode_credentials) and returns a two-element array
184
+ # where the first element is the username and the second is the password. If the encoded
185
+ # credentials are missing or can't be decoded properly, ["", ""] is returned. This allows
186
+ # you to write:
187
+ #
188
+ # un, pw = Api.decode_credentials(creds)
189
+ # raise "Please supply your username and password" if un.blank? || pw.blank?
190
+ #
191
+ def self.decode_credentials(encoded)
192
+ return ["", ""] unless encoded
193
+ username, password = ::Base64.decode64(encoded).split(':', 2)
194
+ [username || "", password || ""]
195
+ end
196
+
197
+ #
198
+ # Performs authorisation against the Auth service. The +token+ must be a token received as a
199
+ # result of a prior authentication operation. The args should be in the form
200
+ #
201
+ # query: "service:controller:hyperlink:verb:app:context"
202
+ #
203
+ # e.g.
204
+ #
205
+ # Api.permitted?(@token, query: "cms:texts:self:GET:*:*")
206
+ #
207
+ # Api.authorization_string can be used to produce the query string.
208
+ #
209
+ # Returns the HTTP response as-is, allowing the caller to examine the status code and
210
+ # messages, and also the body.
211
+ #
212
+ def self.permitted?(token, args={})
213
+ raise unless token
214
+ response = Api.get(:auth, "/authentications/#{token}", args)
215
+ response
216
+ end
217
+
218
+
219
+ #
220
+ # Returns an authorisation string suitable for use in calls to Api.permitted?.
221
+ # The +extra_actions+ arg holds the extra actions as defined in the Ocean controller; it must
222
+ # be included here so that actions can be mapped to the proper hyperlink and verb.
223
+ # The +controller+ and +action+ args are mandatory. The +app+ and +context+ args are optional and will
224
+ # default to "*". The last arg, +service+, defaults to the name of the service itself.
225
+ #
226
+ def self.authorization_string(extra_actions, controller, action, app="*", context="*", service=APP_NAME)
227
+ app = '*' if app.blank?
228
+ context = '*' if context.blank?
229
+ hyperlink, verb = Api.map_authorization(extra_actions, controller, action)
230
+ "#{service}:#{controller}:#{hyperlink}:#{verb}:#{app}:#{context}"
231
+ end
232
+
233
+
234
+ #
235
+ # These are the default controller actions. The purpose of this constant is to map action
236
+ # names to hyperlink and HTTP method (for authorisation purposes). Don't be alarmed by the
237
+ # non-standard GET* - it's purely symbolic and is never used as an actual HTTP method.
238
+ # We need it to differentiate between a GET of a member and a GET of a collection of members.
239
+ # The +extra_actions+ keyword in +ocean_resource_controller+ follows the same format.
240
+ #
241
+ DEFAULT_ACTIONS = {
242
+ 'show' => ['self', 'GET'],
243
+ 'index' => ['self', 'GET*'],
244
+ 'create' => ['self', 'POST'],
245
+ 'update' => ['self', 'PUT'],
246
+ 'destroy' => ['self', 'DELETE'],
247
+ 'connect' => ['connect', 'PUT'],
248
+ 'disconnect' => ['connect', 'DELETE']
249
+ }
250
+
251
+ #
252
+ # Returns the hyperlink and HTTP method to use for an +action+ in a certain +controller+.
253
+ # First, the +DEFAULT_ACTIONS+ are searched, then any extra actions defined for the
254
+ # controller. Raises an exception if the action can't be found.
255
+ #
256
+ def self.map_authorization(extra_actions, controller, action)
257
+ DEFAULT_ACTIONS[action] ||
258
+ extra_actions[controller][action] ||
259
+ raise #"The #{controller} lacks an extra_action declaration for #{action}"
260
+ end
261
+
262
+
263
+ end