the_garage 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +162 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/javascripts/garage/application.js +16 -0
  6. data/app/assets/javascripts/garage/docs/console.js.coffee +90 -0
  7. data/app/assets/javascripts/garage/docs/jquery.colorbox.js +1026 -0
  8. data/app/assets/stylesheets/garage/application.css +14 -0
  9. data/app/assets/stylesheets/garage/colorbox.scss +62 -0
  10. data/app/assets/stylesheets/garage/style.scss +59 -0
  11. data/app/assets/stylesheets/vendor/bootstrap.min.css +9 -0
  12. data/app/controllers/garage/application_controller.rb +4 -0
  13. data/app/controllers/garage/docs/resources_controller.rb +103 -0
  14. data/app/controllers/garage/meta/docs_controller.rb +20 -0
  15. data/app/controllers/garage/meta/services_controller.rb +20 -0
  16. data/app/helpers/garage/application_helper.rb +4 -0
  17. data/app/helpers/garage/docs/resources_helper.rb +24 -0
  18. data/app/models/garage/hash_representer.rb +11 -0
  19. data/app/views/garage/docs/resources/_layout_navigation.html.haml +5 -0
  20. data/app/views/garage/docs/resources/_navigation.html.haml +6 -0
  21. data/app/views/garage/docs/resources/callback.html.haml +5 -0
  22. data/app/views/garage/docs/resources/console.html.haml +45 -0
  23. data/app/views/garage/docs/resources/index.html.haml +2 -0
  24. data/app/views/garage/docs/resources/show.html.haml +16 -0
  25. data/app/views/layouts/garage/application.html.haml +26 -0
  26. data/config/routes.rb +0 -0
  27. data/lib/garage/app_responder.rb +22 -0
  28. data/lib/garage/authorizable.rb +26 -0
  29. data/lib/garage/config.rb +76 -0
  30. data/lib/garage/controller_helper.rb +110 -0
  31. data/lib/garage/docs/anchor_building.rb +28 -0
  32. data/lib/garage/docs/application.rb +24 -0
  33. data/lib/garage/docs/config.rb +61 -0
  34. data/lib/garage/docs/console_link_building.rb +14 -0
  35. data/lib/garage/docs/document.rb +141 -0
  36. data/lib/garage/docs/engine.rb +35 -0
  37. data/lib/garage/docs/example.rb +26 -0
  38. data/lib/garage/docs/renderer.rb +17 -0
  39. data/lib/garage/docs/toc_renderer.rb +14 -0
  40. data/lib/garage/docs.rb +9 -0
  41. data/lib/garage/exceptions.rb +49 -0
  42. data/lib/garage/hypermedia_filter.rb +44 -0
  43. data/lib/garage/hypermedia_responder.rb +120 -0
  44. data/lib/garage/meta/engine.rb +16 -0
  45. data/lib/garage/meta/remote_service.rb +78 -0
  46. data/lib/garage/meta.rb +6 -0
  47. data/lib/garage/meta_resource.rb +17 -0
  48. data/lib/garage/nested_field_query.rb +183 -0
  49. data/lib/garage/optional_response_body_responder.rb +16 -0
  50. data/lib/garage/paginating_responder.rb +113 -0
  51. data/lib/garage/permission.rb +13 -0
  52. data/lib/garage/permissions.rb +75 -0
  53. data/lib/garage/representer.rb +214 -0
  54. data/lib/garage/resource_casting_responder.rb +13 -0
  55. data/lib/garage/restful_actions.rb +219 -0
  56. data/lib/garage/strategy/access_token.rb +57 -0
  57. data/lib/garage/strategy/auth_server.rb +200 -0
  58. data/lib/garage/strategy/no_authentication.rb +13 -0
  59. data/lib/garage/strategy/test.rb +44 -0
  60. data/lib/garage/strategy.rb +4 -0
  61. data/lib/garage/test/migrator.rb +31 -0
  62. data/lib/garage/token_scope.rb +134 -0
  63. data/lib/garage/utils.rb +28 -0
  64. data/lib/garage/version.rb +3 -0
  65. data/lib/garage.rb +23 -0
  66. metadata +275 -0
@@ -0,0 +1,35 @@
1
+ require "rails"
2
+ require "haml"
3
+ require "sass-rails"
4
+ require "coffee-rails"
5
+ require "redcarpet"
6
+
7
+ require "garage/docs/anchor_building"
8
+ require "garage/docs/console_link_building"
9
+ require "garage/docs/renderer"
10
+ require "garage/docs/toc_renderer"
11
+ require "garage/docs/application"
12
+ require "garage/docs/document"
13
+ require "garage/docs/example"
14
+
15
+ module Garage
16
+ module Docs
17
+ class Engine < ::Rails::Engine
18
+ isolate_namespace Garage::Docs
19
+
20
+ initializer "garage.docs.engine.routes" do
21
+ Engine.routes.append do
22
+ root :to => 'resources#index', as: nil
23
+ resources :resources do
24
+ collection do
25
+ get 'console'
26
+ post 'authenticate'
27
+ get 'callback'
28
+ post 'callback'
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ module Garage
2
+ module Docs
3
+ class Example
4
+ def self.build_all(controller, examples)
5
+ examples.compact.map do |resource|
6
+ new(resource, controller)
7
+ end
8
+ end
9
+
10
+ def initialize(resource, controller)
11
+ @resource, @controller = resource, controller
12
+ end
13
+
14
+ def url
15
+ if @resource.is_a?(String)
16
+ @resource
17
+ elsif @resource.respond_to?(:to_proc)
18
+ @resource.to_proc.call(@controller.main_app)
19
+ else
20
+ @resource.represent!
21
+ @resource.link_path_for(:self)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Garage
2
+ module Docs
3
+ class Renderer < ::Redcarpet::Render::HTML
4
+ include ConsoleLinkBuilding
5
+ include AnchorBuilding
6
+
7
+ def header(text, header_level)
8
+ if header_level == 2
9
+ id = to_anchor(text)
10
+ %!<a href="##{id}"><h#{header_level} id="#{id}">#{text} #{build_console_link(text)}</h#{header_level}></a>!
11
+ else
12
+ %!<h#{header_level}>#{text}</h#{header_level}>!
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module Garage
2
+ module Docs
3
+ class TocRenderer < ::Redcarpet::Render::HTML_TOC
4
+ include ConsoleLinkBuilding
5
+ include AnchorBuilding
6
+
7
+ def header(text, header_level)
8
+ return if header_level > 2
9
+
10
+ %'<li><a href="##{to_anchor(text)}">#{text}</a> #{build_console_link(text)}</li>'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require 'garage/docs/engine'
2
+
3
+ module Garage
4
+ module Docs
5
+ def self.application
6
+ @application ||= Garage::Docs::Application.new(Rails.application)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ module Garage
2
+ class HTTPError < ::StandardError
3
+ attr_accessor :status
4
+ def status_code
5
+ Rack::Utils.status_code(status)
6
+ end
7
+ end
8
+
9
+ class BadRequest < HTTPError
10
+ def initialize(error)
11
+ @status = :bad_request
12
+ super(error)
13
+ end
14
+ end
15
+
16
+ class Unauthorized < HTTPError
17
+ attr_reader :user, :action, :resource_class, :scopes
18
+
19
+ def initialize(user, action, resource_class, status = :forbidden, scopes = [])
20
+ @user, @action, @resource_class, @status, @scopes = user, action, resource_class, status, scopes
21
+
22
+ if scopes.empty?
23
+ super "Authorized user is not allowed to take the requested operation #{action} on #{resource_class}"
24
+ else
25
+ super "Insufficient scope to process the requested operation. Missing scope(s): #{scopes.join(", ")}"
26
+ end
27
+ end
28
+ end
29
+
30
+ class PermissionError < Unauthorized; end
31
+ class MissingScopeError < Unauthorized; end
32
+
33
+ class AuthBackendTimeout < HTTPError
34
+ def initialize(open_timeout, read_timeout)
35
+ @status = :internal_server_error
36
+ super("Auth backend timed out: open_timeout=#{open_timeout}, read_timeout=#{read_timeout}")
37
+ end
38
+ end
39
+
40
+ class AuthBackendError < HTTPError
41
+ attr_reader :response
42
+
43
+ def initialize(response)
44
+ @status = :internal_server_error
45
+ @response = response
46
+ super("Auth backend responded error: status=#{response.status_code}")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,44 @@
1
+ module Garage
2
+ class HypermediaFilter
3
+ MIME_DICT = %r[application/vnd\.cookpad\.dictionary\+(json|x-msgpack)]
4
+
5
+ def self.before(controller)
6
+ helper = new(controller)
7
+ controller.representation = helper.dictionary_representation if helper.has_dictionary_mime_type?
8
+ controller.request.format = helper.dictionary_request_format if helper.has_dictionary_mime_type?
9
+ controller.field_selector = helper.field_selector
10
+ rescue Garage::NestedFieldQuery::InvalidQuery
11
+ raise Garage::BadRequest, "Invalid query in ?fields="
12
+ end
13
+
14
+ attr_reader :controller
15
+
16
+ def initialize(controller)
17
+ @controller = controller
18
+ end
19
+
20
+ def field_selector
21
+ Garage::NestedFieldQuery::Selector.build(fields_param)
22
+ end
23
+
24
+ def fields_param
25
+ controller.params[:fields]
26
+ end
27
+
28
+ def dictionary_representation
29
+ :dictionary
30
+ end
31
+
32
+ def dictionary_request_format
33
+ dictionary_match_data[1].sub(/^x-/, "").to_sym
34
+ end
35
+
36
+ def has_dictionary_mime_type?
37
+ dictionary_match_data
38
+ end
39
+
40
+ def dictionary_match_data
41
+ @dictionary_match_data ||= controller.request.format.to_s.match(MIME_DICT)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,120 @@
1
+ require 'oj'
2
+
3
+ module Garage
4
+ module HypermediaResponder
5
+ def display(resource, given_options={})
6
+ given_options[:content_type] = representation.content_type if representation.dictionary?
7
+ super(render(transform(resource)), given_options)
8
+ end
9
+
10
+ def render(data)
11
+ DataRenderer.render(data, dictionary: representation.dictionary?)
12
+ end
13
+
14
+ def transform(resources)
15
+ if resources.respond_to?(:map) && resources.respond_to?(:to_a)
16
+ resources.map {|resource| encode_to_hash(resource, partial: true) }
17
+ else
18
+ encode_to_hash(resources)
19
+ end
20
+ end
21
+
22
+ # Cache resource encoding result if the resource can be identified.
23
+ # Garage try to get resource identifier by acccess the resource
24
+ # `#resource_identifier` or `#id`.
25
+ def encode_to_hash(resource, *args)
26
+ if id = get_resource_identifier(resource)
27
+ cache_key = "#{resource.class.name}:#{id}"
28
+ cache[cache_key] ||= _encode_to_hash(resource, *args)
29
+ else
30
+ _encode_to_hash(resource, *args)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def _encode_to_hash(resource, options = {})
37
+ resource.represent!
38
+ resource.params = controller.params.slice(*resource.class.params)
39
+ resource.partial = options[:partial]
40
+ resource.selector = options[:selector] || controller.field_selector
41
+ resource.render_hash(:responder => self)
42
+ end
43
+
44
+ def cache
45
+ @cache ||= {}
46
+ end
47
+
48
+ def get_resource_identifier(resource)
49
+ case
50
+ when resource.respond_to?(:resource_identifier)
51
+ resource.resource_identifier
52
+ when resource.respond_to?(:id)
53
+ resource.id
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ def representation
60
+ @representation ||= Representation.new(controller)
61
+ end
62
+
63
+ class Representation
64
+ attr_reader :controller
65
+
66
+ def initialize(controller)
67
+ @controller = controller
68
+ end
69
+
70
+ def dictionary?
71
+ controller.representation == :dictionary
72
+ end
73
+
74
+ def content_type
75
+ mime, payload = controller.request.format.to_s.split("/", 2)
76
+ "#{mime}/vnd.cookpad.dictionary+#{payload}"
77
+ end
78
+ end
79
+
80
+ class DataRenderer
81
+ JSON_ESCAPE_TABLE = { "<" => '\u003C', ">" => '\u003E' }.freeze
82
+
83
+ def self.render(*args)
84
+ new(*args).render
85
+ end
86
+
87
+ attr_reader :data, :options
88
+
89
+ def initialize(data, options = {})
90
+ @data, @options = data, options
91
+ end
92
+
93
+ def render
94
+ Oj.dump(converted_data, mode: :compat).gsub(/([<>])/, JSON_ESCAPE_TABLE)
95
+ end
96
+
97
+ private
98
+
99
+ def dictionary?
100
+ !!options[:dictionary]
101
+ end
102
+
103
+ def convertible_to_dictionary?
104
+ dictionary? && data.is_a?(Array) && data.all? {|datum| datum.respond_to?(:[]) }
105
+ end
106
+
107
+ def indexed_data
108
+ data.index_by {|datum| datum["id"] }
109
+ end
110
+
111
+ def converted_data
112
+ if convertible_to_dictionary?
113
+ indexed_data
114
+ else
115
+ data
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,16 @@
1
+ require "garage/meta/remote_service"
2
+
3
+ module Garage
4
+ module Meta
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Garage::Meta
7
+
8
+ initializer "garage.meta.engine.routes" do
9
+ Engine.routes.append do
10
+ resources :services
11
+ resources :docs
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,78 @@
1
+ module Garage
2
+ module Meta
3
+ class RemoteService
4
+ class << self
5
+ def configure(&block)
6
+ @config = Config.new
7
+ @config.instance_eval(&block)
8
+ end
9
+
10
+ def configuration
11
+ @config or raise "Garage::Meta::RemoteService.configure must be called in initializer"
12
+ end
13
+
14
+ def all
15
+ configuration.services
16
+ end
17
+
18
+ def build_permissions(perms, other, target)
19
+ perms.permits! :read
20
+ end
21
+ end
22
+
23
+ include Garage::Representer
24
+ include Garage::Authorizable
25
+
26
+ property :namespace
27
+ property :name
28
+ property :endpoint
29
+ property :alternate_endpoints
30
+
31
+ attr_accessor :namespace, :name, :endpoint
32
+
33
+ def alternate_endpoints
34
+ @alternate_endpoints ||= {}
35
+ end
36
+
37
+ class Config
38
+ attr_reader :services
39
+
40
+ def initialize
41
+ @services = []
42
+ end
43
+
44
+ def service(&block)
45
+ service = ServiceDSL.new
46
+ service.instance_eval(&block)
47
+ @services << service.build
48
+ end
49
+ end
50
+
51
+ class ServiceDSL
52
+ def initialize
53
+ @service = RemoteService.new
54
+ end
55
+
56
+ def namespace(arg)
57
+ @service.namespace = arg.to_s
58
+ end
59
+
60
+ def name(arg)
61
+ @service.name = arg
62
+ end
63
+
64
+ def endpoint(arg)
65
+ @service.endpoint = arg
66
+ end
67
+
68
+ def alternate_endpoint(rel, url)
69
+ @service.alternate_endpoints[rel] = url
70
+ end
71
+
72
+ def build
73
+ @service
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,6 @@
1
+ require 'garage/meta/engine'
2
+
3
+ module Garage
4
+ module Meta
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ # Public: proxy object to handle model Class as a resource
2
+ module Garage
3
+ class MetaResource
4
+ include Garage::Authorizable
5
+
6
+ attr_reader :resource_class
7
+
8
+ def initialize(resource_class, args = {})
9
+ @resource_class = resource_class
10
+ @args = args
11
+ end
12
+
13
+ def build_permissions(perms, user)
14
+ resource_class.build_permissions(perms, user, @args)
15
+ end
16
+ end
17
+ end