ocean-rails 1.14.0

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