intermodal 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +1 -1
  5. data/README +6 -4
  6. data/Rakefile +6 -0
  7. data/intermodal.gemspec +51 -0
  8. data/lib/generators/intermodal_generator.rb +8 -0
  9. data/lib/intermodal.rb +122 -0
  10. data/lib/intermodal/api.rb +246 -0
  11. data/lib/intermodal/api/configuration.rb +40 -0
  12. data/lib/intermodal/api/railties.rb +21 -0
  13. data/lib/intermodal/concerns/acceptors/named_resource.rb +12 -0
  14. data/lib/intermodal/concerns/acceptors/resource.rb +12 -0
  15. data/lib/intermodal/concerns/controllers/accountability.rb +17 -0
  16. data/lib/intermodal/concerns/controllers/anonymous.rb +24 -0
  17. data/lib/intermodal/concerns/controllers/authenticatable.rb +24 -0
  18. data/lib/intermodal/concerns/controllers/paginated_collection.rb +25 -0
  19. data/lib/intermodal/concerns/controllers/presentation.rb +17 -0
  20. data/lib/intermodal/concerns/controllers/resource.rb +51 -0
  21. data/lib/intermodal/concerns/controllers/resource_linking.rb +58 -0
  22. data/lib/intermodal/concerns/let.rb +21 -0
  23. data/lib/intermodal/concerns/models/access_credential.rb +28 -0
  24. data/lib/intermodal/concerns/models/account.rb +16 -0
  25. data/lib/intermodal/concerns/models/accountability.rb +47 -0
  26. data/lib/intermodal/concerns/models/db_access_token.rb +29 -0
  27. data/lib/intermodal/concerns/models/has_parent_resource.rb +45 -0
  28. data/lib/intermodal/concerns/models/presentation.rb +25 -0
  29. data/lib/intermodal/concerns/models/redis_access_token.rb +60 -0
  30. data/lib/intermodal/concerns/models/resource_linking.rb +126 -0
  31. data/lib/intermodal/concerns/models/sanitize_html.rb +35 -0
  32. data/lib/intermodal/concerns/presenters/named_resource.rb +12 -0
  33. data/lib/intermodal/concerns/presenters/resource.rb +14 -0
  34. data/lib/intermodal/concerns/rails/rails_3_stack.rb +42 -0
  35. data/lib/intermodal/concerns/rails/rails_4_stack.rb +17 -0
  36. data/lib/intermodal/concerns/rails/use_warden.rb +21 -0
  37. data/lib/intermodal/config.rb +15 -0
  38. data/lib/intermodal/configuration.rb +11 -0
  39. data/lib/intermodal/controllers/api_controller.rb +26 -0
  40. data/lib/intermodal/controllers/linking_resource_controller.rb +8 -0
  41. data/lib/intermodal/controllers/nested_resource_controller.rb +18 -0
  42. data/lib/intermodal/controllers/resource_controller.rb +11 -0
  43. data/lib/intermodal/dsl/controllers.rb +125 -0
  44. data/lib/intermodal/dsl/mapping.rb +79 -0
  45. data/lib/intermodal/dsl/presentation_helpers.rb +107 -0
  46. data/lib/intermodal/mapping/acceptor.rb +2 -2
  47. data/lib/intermodal/mapping/mapper.rb +39 -13
  48. data/lib/intermodal/mapping/presenter.rb +12 -6
  49. data/lib/intermodal/proxies/linking_resources.rb +58 -0
  50. data/lib/intermodal/proxies/will_paginate.rb +85 -0
  51. data/lib/intermodal/rack/auth.rb +29 -0
  52. data/lib/intermodal/rack/dummy_store.rb +24 -0
  53. data/lib/intermodal/rack/rescue.rb +82 -0
  54. data/lib/intermodal/responders/linking_resource_responder.rb +21 -0
  55. data/lib/intermodal/responders/resource_responder.rb +64 -0
  56. data/lib/intermodal/rspec/acceptors.rb +79 -0
  57. data/lib/intermodal/rspec/models/accountability.rb +114 -0
  58. data/lib/intermodal/rspec/models/has_parent_resource.rb +132 -0
  59. data/lib/intermodal/rspec/models/resource_linking.rb +234 -0
  60. data/lib/intermodal/rspec/models/sanitization.rb +84 -0
  61. data/lib/intermodal/rspec/presenters.rb +92 -0
  62. data/lib/intermodal/rspec/requests/authenticated_requests.rb +17 -0
  63. data/lib/intermodal/rspec/requests/linked_resources.rb +180 -0
  64. data/lib/intermodal/rspec/requests/paginated_collection.rb +60 -0
  65. data/lib/intermodal/rspec/requests/rack.rb +142 -0
  66. data/lib/intermodal/rspec/requests/request_validations.rb +36 -0
  67. data/lib/intermodal/rspec/requests/resources.rb +275 -0
  68. data/lib/intermodal/rspec/requests/rfc2616_status_codes.rb +51 -0
  69. data/lib/intermodal/rspec/validators.rb +86 -0
  70. data/lib/intermodal/validators/account_validator.rb +27 -0
  71. data/lib/intermodal/validators/different_account_validator.rb +27 -0
  72. data/lib/intermodal/version.rb +3 -0
  73. data/spec/mapping/acceptors_spec.rb +142 -0
  74. data/spec/mapping/presenters_spec.rb +186 -0
  75. data/spec/models/accountability_spec.rb +13 -0
  76. data/spec/models/has_parent_resource_spec.rb +18 -0
  77. data/spec/models/resource_linking_spec.rb +21 -0
  78. data/spec/proxies/will_paginate_spec.rb +163 -0
  79. data/spec/rack/auth_spec.rb +51 -0
  80. data/spec/requests/linked_resources.rb +37 -0
  81. data/spec/requests/nested_resources_spec.rb +54 -0
  82. data/spec/requests/resources_spec.rb +50 -0
  83. data/spec/spec_helper.rb +53 -0
  84. data/spec/support/api.rb +50 -0
  85. data/spec/support/app/class_builder.rb +41 -0
  86. data/spec/support/app/db/adapter_helper.rb +53 -0
  87. data/spec/support/app/db/authentication_schema_helper.rb +62 -0
  88. data/spec/support/app/db/migration_helper.rb +44 -0
  89. data/spec/support/app/schema.rb +101 -0
  90. data/spec/support/application.rb +23 -0
  91. data/spec/support/blueprints.rb +41 -0
  92. data/spec/support/epiphyte.rb +29 -0
  93. metadata +393 -52
  94. data/lib/intermodal/base.rb +0 -13
  95. data/lib/intermodal/declare_controllers.rb +0 -102
  96. data/lib/intermodal/mapping.rb +0 -4
  97. data/lib/intermodal/mapping/dsl.rb +0 -76
@@ -5,23 +5,29 @@ module Intermodal
5
5
  self._mapping_strategy = INCLUDE_NILS
6
6
 
7
7
  class << self
8
- alias exclude_from_presentation exclude_properties
8
+ alias exclude_from_presentation exclude_properties
9
9
 
10
10
  # Convenience alias for maps
11
11
  # Examples:
12
12
  #
13
13
  # presents :name
14
- # presents :name, :as => lambda { |o| o.name.camelize }
15
- # presents :name, :as => [ :first_name, :last_name, :suffix, :prefix ]
16
- #
14
+ # presents :name, :with => lambda { |o| o.name.camelize }
15
+ # presents :name, :with => [ :first_name, :last_name, :suffix, :prefix ]
16
+
17
17
  alias presents maps
18
18
 
19
19
  def model_name(resource)
20
20
  resource.class.name.demodulize.underscore
21
21
  end
22
22
 
23
- def call(resource)
24
- { model_name(resource) => map_attributes(resource) }
23
+ def call(resource, options = {})
24
+ _scope = options[:scope] || :default
25
+
26
+ if options[:root] || options[:always_nest_collections]
27
+ { (options[:root].is_a?(TrueClass) || options[:root].nil? ? model_name(resource) : options[:root]) => map_attributes(resource, _scope) }
28
+ else
29
+ map_attributes(resource, _scope)
30
+ end
25
31
  end
26
32
  end
27
33
  end
@@ -0,0 +1,58 @@
1
+ module Intermodal
2
+ module Proxies
3
+ # This class is necessary to create the correct output for linked resources.
4
+ # Example:
5
+ #
6
+ # books n-to-n authors
7
+ # Book #1 has 3 authors, Author #1, #2, and #3
8
+ #
9
+ # Expected output:
10
+ #
11
+ # { "books": { "author_ids": [ 1, 2, 3 ] } }
12
+ #
13
+ # <books>
14
+ # <author_ids>
15
+ # <author_id>1</author_id>
16
+ # <author_id>2</author_id>
17
+ # <author_id>3</author_id>
18
+ # </author_ids>
19
+ # </books>
20
+ #
21
+ class LinkingResources
22
+ attr_accessor :parent_resource_name, :linked_resource_name, :collection, :parent_id
23
+ delegate :to_a, :to => :collection
24
+
25
+ # USAGE:
26
+ # Intermodal::Proxies::LinkingResources.new(:parent, :to => :linked_resources, :with => collection)
27
+ def initialize(parent_resource_name, options = {})
28
+ @parent_resource_name = parent_resource_name
29
+ @collection = options[:with]
30
+ @linked_resource_name = options[:to]
31
+ @parent_id = (options[:parent_id] ? options[:parent_id].to_i : nil ) # nil should be nil, not 0
32
+ end
33
+
34
+ def to_json(options = {})
35
+ as_json(options).to_json
36
+ end
37
+
38
+ def as_json(options = {})
39
+ root = options[:root] || parent_resource_name
40
+ (root ? { root => presentation } : presentation)
41
+ end
42
+
43
+ def to_xml(options = {})
44
+ root = options[:root] || parent_resource_name
45
+ presentation.to_xml(:root => root)
46
+ end
47
+
48
+ def presentation
49
+ { linked_resource_element_name => collection.to_a, :id => parent_id }
50
+ end
51
+
52
+ def linked_resource_element_name
53
+ "#{linked_resource_name.to_s.singularize}_ids"
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ module Intermodal
2
+ module Proxies
3
+ module WillPaginate
4
+ module Collection
5
+
6
+ def pagination_info
7
+ { :page => self.current_page.to_i,
8
+ :per_page => self.per_page.to_i,
9
+ :total_pages => self.total_pages,
10
+ :total_entries => self.total_entries }
11
+ end
12
+
13
+ # TODO: This needs its own spec
14
+ def as_json(*args)
15
+ options = args.extract_options!
16
+ _root = options.delete(:root)
17
+ _collection_name = _root || :collection
18
+
19
+ # Scrub out everything else
20
+ presenter_options = {
21
+ :root => (options[:always_nest_collections] && _root ? _collection_name.to_s.singularize : nil),
22
+ :always_nest_collections => options[:always_nest_collections],
23
+ :scope => options[:scope],
24
+ :presenter => options[:presenter] }
25
+ pagination_info.merge({ _collection_name => self.to_a.as_json(presenter_options)})
26
+ end
27
+
28
+ # TODO: This needs its own spec
29
+ # TODO: Should be refactored to have clearer code
30
+ def to_xml(*args)
31
+ options = args.extract_options!
32
+ _collection = self
33
+
34
+ # For a paginated collection of say, books, we want the following output:
35
+ #
36
+ # <books>
37
+ # <collection>
38
+ # <book>
39
+ # <title> ... </title>
40
+ # ...
41
+ #
42
+ # Since Rails 3.0 magically assumes that a hash
43
+ #
44
+ # { :collection => [ {:title => ' ... ' } ] }
45
+ #
46
+ # outputs to
47
+ #
48
+ # <collection>
49
+ # <collection>
50
+ # <title> ... </title>
51
+ #
52
+ # We need to use a custom builder. We use very kludgy code, by calling #to_xml
53
+ # again, but override builder so it will nest things properly.
54
+ #
55
+ # Actually, it is also depending on some magic. Presenter/API options are saved
56
+ # in the builder class, so it uses that. Which means this isn't side-effect-free
57
+ # code. Ugh.
58
+ #
59
+ # Ultimately, this needs to be more modular so that API writers can specify their
60
+ # own builder. For now, we'll dictate the auto-generated XML and wait until someone
61
+ # complains about it in the Github Issues tracker.
62
+ collection_builder = proc do |opts, key|
63
+ _builder = opts[:builder]
64
+
65
+ _builder.collection do
66
+ _collection.to_a.map do |r|
67
+ r.presentation(options.except(:root)).
68
+ to_xml(
69
+ :root => r.class.name.demodulize.underscore,
70
+ :builder => opts[:builder],
71
+ :skip_instruct => true )
72
+ end
73
+ end
74
+ end
75
+
76
+ # Merge information such as current page, total pages, total entries, etc.
77
+ pagination_info.
78
+ merge({ :collection => collection_builder }).
79
+ to_xml(options)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -0,0 +1,29 @@
1
+ module Intermodal
2
+ module Rack
3
+ class Auth
4
+ UNAUTHORIZED = [401, {}, []]
5
+ IDENTITY='HTTP_X_AUTH_IDENTITY'
6
+ KEY='HTTP_X_AUTH_KEY'
7
+
8
+ class << self
9
+ def valid?(env)
10
+ env[IDENTITY] && env[KEY]
11
+ end
12
+
13
+ # USAGE:
14
+ #
15
+ # (1) Define ::AccessCredential.authenticate!(identity, key) and returns an account object
16
+ # (2) Define ::AccessToken.generate!(account) and inserts token for authentication
17
+ # You can inherit from Intermodal::Auth::AccessToken
18
+ # (3) Add to routes:
19
+ # match '/auth', :to => Intermodal::Rack::Auth
20
+
21
+ def call(env)
22
+ return UNAUTHORIZED unless valid?(env) && ( account = ::AccessCredential.authenticate!(env[IDENTITY], env[KEY]))
23
+ access_token = ::AccessToken.generate!(account)
24
+ [ 204, { 'X-Auth-Token' => access_token.to_s }, []]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ require 'action_dispatch/middleware/session/abstract_store'
2
+
3
+ module Intermodal
4
+ module Rack
5
+ # A dummy store that does nothing, but satisfies Warden's session store requirement
6
+ # This allows us to use the x_auth_token strategy
7
+ class DummyStore < ::ActionDispatch::Session::AbstractStore
8
+
9
+ def get_session(env, sid)
10
+ [nil, nil]
11
+ end
12
+
13
+ # Set a session in the cache.
14
+ def set_session(env, sid, session, options)
15
+ nil
16
+ end
17
+
18
+ # Remove a session from the cache.
19
+ def destroy_session(env, sid, options)
20
+ nil
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ #require 'action_dispatch/http/mime_type'
2
+
3
+ module Intermodal
4
+ module Rack
5
+ class Rescue
6
+ include ActionController::Rescue
7
+
8
+ def self.watch(x)
9
+ ap x if defined? ap
10
+ end
11
+
12
+ def watch(x)
13
+ self.class.watch(x)
14
+ end
15
+
16
+ rescue_from Exception do |exception|
17
+ if defined? Rails and Rails.env == 'production'
18
+ [500, {}, [ "Unexpected error. Please contact support." ] ]
19
+ else
20
+ [500, {}, [ "Exception: #{exception.message}", "\n\n", exception.backtrace ].tap(&method(:watch)) ]
21
+ end
22
+ end
23
+
24
+ rescue_from Intermodal::BadRequest do |exception|
25
+ [400, {}, [ 'Bad Request' ] ]
26
+ end
27
+
28
+ rescue_from ActiveRecord::RecordNotFound do |exception|
29
+ [404, {}, [ 'Not Found' ] ]
30
+ end
31
+
32
+ rescue_from ActionController::RoutingError do |exception|
33
+ if defined? Rails and Rails.env == 'production'
34
+ [404, {}, ['Not Found'] ]
35
+ else
36
+ [500, {}, [ "Exception: #{exception.message}", "\n", caller.join("\n"), "\n", exception.backtrace ].tap(&method(:watch)) ]
37
+ end
38
+ end
39
+
40
+ # TODO: Hack. Untested.
41
+ if defined? ActiveResource::ResourceNotFound
42
+ rescue_from ActiveResource::ResourceNotFound do |exception|
43
+ [404, {}, [ 'Not Found' ] ]
44
+ end
45
+ end
46
+
47
+ rescue_from ActionDispatch::ParamsParser::ParseError do |exception|
48
+ [400, { 'Content-Type' => content_type(:json) }, { :parse_error => exception.message }.to_json]
49
+ end
50
+
51
+ rescue_from MultiJson::DecodeError do |exception|
52
+ [400, { 'Content-Type' => content_type(:json) }, { :parse_error => exception.message }.to_json]
53
+ end
54
+
55
+ rescue_from 'REXML::ParseException' do |exception|
56
+ [400, { 'Content-Type' => content_type(:xml) }, { :parse_error => exception.message }.to_xml]
57
+ end
58
+
59
+ def content_type(format)
60
+ "#{Mime::Type.lookup_by_extension(format)}; charset=utf-8"
61
+ end
62
+
63
+ def initialize(app, &block)
64
+ @app = app
65
+ self.class.instance_eval(&block) if block_given?
66
+ end
67
+
68
+ def call(env)
69
+ @app.call(env)
70
+ rescue Exception => exception
71
+ rescue_with_handler(exception) || raise(exception)
72
+ end
73
+
74
+ # Overrides ActiveSuppor::Rescuable#rescue_with_handler
75
+ # All handlers *MUST* respond with a valid Rack response
76
+ def rescue_with_handler(exception)
77
+ return unless handler = handler_for_rescue(exception)
78
+ handler.arity != 0 ? handler.call(exception) : handler.call
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,21 @@
1
+ require 'action_controller/responder'
2
+
3
+ module Intermodal
4
+ class LinkingResourceResponder < ResourceResponder
5
+
6
+ # This is the common behavior for "API" requests, like :xml and :json.
7
+ def respond
8
+ if get?
9
+ display resource, :root => presentation_root
10
+ elsif has_errors?
11
+ display resource.errors, :status => :unprocessable_entity
12
+ elsif post?
13
+ display resource, :status => :created
14
+ #:location => api_location # Taken out because it requires some additional URL definitions
15
+ else
16
+ head :ok
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'action_controller/responder'
2
+
3
+
4
+ module Intermodal
5
+ class ResourceResponder < ActionController::Responder
6
+ include Intermodal::Let
7
+
8
+ attr_accessor :options
9
+
10
+ let(:presenter) { options[:presenter] || controller.send(:presenter) }
11
+
12
+ let(:presentation_root) { options[:presentation_root] || controller_send(:presentation_root) }
13
+ let(:presentation_scope) { options[:presentation_scope] || controller_send(:presentation_scope) }
14
+ let(:always_nest_collections) { options[:always_nest_collections] || controller_send(:always_nest_collections) || false }
15
+
16
+ def initialize(controller, resources, options={})
17
+ super(controller, resources, options)
18
+ @options = options
19
+ end
20
+
21
+ # This is the common behavior for "API" requests, like :xml and :json.
22
+ def respond
23
+ return head :status => 404 unless resource
24
+ if get?
25
+ display resource,
26
+ root: presentation_root,
27
+ presenter: presenter,
28
+ scope: presentation_scope,
29
+ always_nest_collections: always_nest_collections
30
+
31
+ elsif has_errors?
32
+ display resource.errors,
33
+ root: presentation_root,
34
+ status: :unprocessable_entity,
35
+ presenter: presenter,
36
+ scope: presentation_scope,
37
+ always_nest_collections: always_nest_collections
38
+ elsif put?
39
+ display resource,
40
+ root: presentation_root,
41
+ presenter: presenter,
42
+ scope: presentation_scope,
43
+ always_nest_collections: always_nest_collections
44
+ elsif post?
45
+ display resource,
46
+ root: presentation_root,
47
+ status: :created,
48
+ presenter: presenter,
49
+ scope: presentation_scope,
50
+ always_nest_collections: always_nest_collections
51
+ #:location => api_location # Taken out because it requires some additional URL definitions
52
+ else
53
+ head status: 204
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def controller_send(_method)
60
+ controller.respond_to?(_method, true) ? controller.send(_method) : nil
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,79 @@
1
+ module Intermodal
2
+ module RSpec
3
+ module Acceptors
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ extend ::Intermodal::RSpec::Macros::Acceptors
7
+
8
+ let(:resource_name) { subject.name.underscore }
9
+ let(:acceptor) { api.acceptors[resource_name.to_sym] }
10
+ let(:random_field_data) { "Accepted Field #{rand(100000)}" }
11
+
12
+ def acceptance_for(input)
13
+ acceptor.call(input.with_indifferent_access)
14
+ end
15
+
16
+ def expects_acceptance(input, expectation)
17
+ acceptance_for(input).should eql(expectation)
18
+ end
19
+
20
+ def expects_rejection(input, expectation)
21
+ acceptance_for(input).should_not eql(expectation)
22
+ end
23
+ end
24
+ end
25
+
26
+ module Macros
27
+ module Acceptors
28
+ def concerned_with_acceptance(_model, &blk)
29
+ describe _model do
30
+ context "when concerned with acceptance" do
31
+ subject { _model }
32
+ instance_eval(&blk) if blk
33
+ end
34
+ end
35
+ end
36
+
37
+ def imposes(*fields)
38
+ fields.each do |field|
39
+ it "should accept #{field}" do
40
+ expects_acceptance({ field => random_field_data }, { field.to_sym => random_field_data })
41
+ end
42
+ end
43
+ end
44
+
45
+ def rejects(*fields)
46
+ fields.each do |field|
47
+ it "should reject #{field}" do
48
+ expects_rejection({ field => random_field_data }, { field.to_sym => random_field_data })
49
+ end
50
+ end
51
+ end
52
+
53
+ def imposes_named_resource
54
+ context 'when exposing named resource fields' do
55
+ imposes 'name'
56
+ end
57
+ end
58
+
59
+ def imposes_linked_resources(linked_resources_name, options = {})
60
+ _linked_resource_name, _linking_association = linked_resources_name.to_s.singularize, options[:with]
61
+ pending "when exposing linked resources, #{_linked_resource_name}" do
62
+ let(:linked_resource_ids_name) { :"#{_linked_resource_name}_ids" }
63
+ let(:linking_association) { :"#{_linking_association}" }
64
+ let(:linked_resource_ids) { subject.send(linking_association).send("to_#{linked_resource_ids_name}") }
65
+
66
+ it { should accept "#{_linked_resource_name}_ids" }
67
+ it "should accept \"#{_linked_resource_name}_ids\" as a collection of ids" do
68
+ resource
69
+ (presenter[linked_resource_ids_name].to_a & linked_resource_ids.to_a).should be_empty
70
+ end
71
+
72
+ pending "should only accept \"#{_linked_resource_name}_ids\" scoped to account"
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end