shaf 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/iana_link_relations.csv.gz +0 -0
  5. data/lib/shaf.rb +6 -0
  6. data/lib/shaf/alps/attribute_serializer.rb +41 -0
  7. data/lib/shaf/alps/json_serializer.rb +50 -0
  8. data/lib/shaf/alps/relation_serializer.rb +70 -0
  9. data/lib/shaf/api_doc/link_relations.rb +77 -0
  10. data/lib/shaf/app.rb +12 -5
  11. data/lib/shaf/authenticator.rb +56 -0
  12. data/lib/shaf/authenticator/base.rb +161 -0
  13. data/lib/shaf/authenticator/basic_auth.rb +25 -0
  14. data/lib/shaf/authenticator/challenge.rb +32 -0
  15. data/lib/shaf/authenticator/parameter.rb +31 -0
  16. data/lib/shaf/authenticator/request.rb +17 -0
  17. data/lib/shaf/command/console.rb +1 -1
  18. data/lib/shaf/command/generate.rb +5 -2
  19. data/lib/shaf/command/new.rb +20 -7
  20. data/lib/shaf/command/templates/Gemfile.erb +1 -0
  21. data/{templates/config/settings.yml → lib/shaf/command/templates/config/settings.yml.erb} +1 -5
  22. data/lib/shaf/errors.rb +11 -0
  23. data/lib/shaf/extensions.rb +3 -3
  24. data/lib/shaf/extensions/api_routes.rb +60 -0
  25. data/lib/shaf/extensions/authorize.rb +11 -9
  26. data/lib/shaf/extensions/log.rb +1 -1
  27. data/lib/shaf/extensions/resource_uris.rb +139 -63
  28. data/lib/shaf/extensions/symbolic_routes.rb +22 -19
  29. data/lib/shaf/formable.rb +1 -2
  30. data/lib/shaf/formable/form.rb +1 -1
  31. data/lib/shaf/generator.rb +2 -0
  32. data/lib/shaf/generator/base.rb +2 -3
  33. data/lib/shaf/generator/controller.rb +11 -7
  34. data/lib/shaf/generator/doc.rb +17 -0
  35. data/lib/shaf/generator/forms.rb +1 -0
  36. data/lib/shaf/generator/helper.rb +2 -1
  37. data/lib/shaf/generator/migration/base.rb +7 -3
  38. data/lib/shaf/generator/migration/type.rb +4 -26
  39. data/lib/shaf/generator/migration/types.rb +45 -16
  40. data/lib/shaf/generator/model.rb +1 -2
  41. data/lib/shaf/generator/profile.rb +52 -0
  42. data/lib/shaf/generator/serializer.rb +38 -73
  43. data/lib/shaf/generator/templates/api/policy.rb.erb +2 -2
  44. data/lib/shaf/generator/templates/api/profile.rb.erb +16 -0
  45. data/lib/shaf/generator/templates/api/serializer.rb.erb +2 -2
  46. data/lib/shaf/generator/templates/spec/integration_spec.rb.erb +1 -2
  47. data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +5 -5
  48. data/lib/shaf/helpers.rb +4 -0
  49. data/lib/shaf/helpers/authentication.rb +79 -0
  50. data/lib/shaf/helpers/cache_control.rb +1 -2
  51. data/lib/shaf/helpers/json_html.rb +58 -18
  52. data/lib/shaf/helpers/payload.rb +27 -41
  53. data/lib/shaf/helpers/vary.rb +8 -0
  54. data/lib/shaf/logger.rb +12 -0
  55. data/lib/shaf/parser.rb +65 -0
  56. data/lib/shaf/parser/base.rb +44 -0
  57. data/lib/shaf/parser/form_data.rb +15 -0
  58. data/lib/shaf/parser/json.rb +26 -0
  59. data/lib/shaf/profile.rb +110 -0
  60. data/lib/shaf/profile/attribute.rb +29 -0
  61. data/lib/shaf/profile/evaluator.rb +46 -0
  62. data/lib/shaf/profile/relation.rb +29 -0
  63. data/lib/shaf/profile/unique_id.rb +58 -0
  64. data/lib/shaf/profiles.rb +42 -0
  65. data/lib/shaf/profiles/shaf_basic.rb +20 -0
  66. data/lib/shaf/profiles/shaf_error.rb +48 -0
  67. data/lib/shaf/profiles/shaf_form.rb +109 -0
  68. data/lib/shaf/responder.rb +41 -2
  69. data/lib/shaf/responder/alps_json.rb +25 -0
  70. data/lib/shaf/responder/base.rb +20 -17
  71. data/lib/shaf/responder/hal.rb +62 -7
  72. data/lib/shaf/responder/html.rb +65 -9
  73. data/lib/shaf/responder/problem_json.rb +1 -1
  74. data/lib/shaf/serializer.rb +31 -0
  75. data/lib/shaf/settings.rb +25 -12
  76. data/lib/shaf/spec.rb +1 -0
  77. data/lib/shaf/spec/authenticator.rb +13 -0
  78. data/lib/shaf/spec/base.rb +1 -1
  79. data/lib/shaf/spec/http_method_utils.rb +1 -1
  80. data/lib/shaf/spec/integration_spec.rb +25 -13
  81. data/lib/shaf/spec/payload_utils.rb +2 -2
  82. data/lib/shaf/supported_http_methods.rb +15 -0
  83. data/lib/shaf/tasks/api_doc_task.rb +24 -3
  84. data/lib/shaf/tasks/routes_task.rb +14 -17
  85. data/lib/shaf/upgrade/manifest.rb +11 -2
  86. data/lib/shaf/upgrade/package.rb +78 -49
  87. data/lib/shaf/upgrade/version.rb +11 -10
  88. data/lib/shaf/utils.rb +19 -5
  89. data/lib/shaf/version.rb +3 -1
  90. data/lib/shaf/yard.rb +34 -0
  91. data/lib/shaf/yard/attribute_method_handler.rb +19 -0
  92. data/lib/shaf/yard/attribute_object.rb +30 -0
  93. data/lib/shaf/yard/base_method_handler.rb +30 -0
  94. data/lib/shaf/yard/link_method_handler.rb +39 -0
  95. data/lib/shaf/yard/link_object.rb +60 -0
  96. data/lib/shaf/yard/nested_attributes.rb +37 -0
  97. data/lib/shaf/yard/parser.rb +64 -0
  98. data/lib/shaf/yard/profile_method_handler.rb +51 -0
  99. data/lib/shaf/yard/profile_object.rb +21 -0
  100. data/lib/shaf/yard/resource_object.rb +55 -0
  101. data/lib/shaf/yard/serializer_handler.rb +27 -0
  102. data/templates/api/controllers/base_controller.rb +0 -10
  103. data/templates/api/controllers/docs_controller.rb +5 -3
  104. data/templates/api/controllers/root_controller.rb +7 -1
  105. data/templates/api/policies/base_policy.rb +2 -0
  106. data/templates/api/serializers/base_serializer.rb +1 -3
  107. data/templates/api/serializers/error_serializer.rb +1 -5
  108. data/templates/api/serializers/form_serializer.rb +1 -5
  109. data/templates/api/serializers/validation_error_serializer.rb +1 -5
  110. data/templates/config.ru +1 -1
  111. data/templates/config/bootstrap.rb +1 -2
  112. data/templates/config/directories.rb +52 -44
  113. data/templates/config/helpers.rb +1 -1
  114. data/templates/config/initializers.rb +52 -8
  115. data/templates/config/initializers/authentication.rb +18 -0
  116. data/templates/config/initializers/db_migrations.rb +2 -2
  117. data/templates/config/initializers/logging.rb +2 -2
  118. data/templates/frontend/assets/css/main.css +33 -1
  119. data/templates/frontend/views/headers.erb +20 -0
  120. data/templates/frontend/views/layout.erb +7 -1
  121. data/templates/frontend/views/payload.erb +1 -0
  122. data/templates/spec/spec_helper.rb +2 -0
  123. data/upgrades/0.5.0.tar.gz +0 -0
  124. data/upgrades/1.0.4.tar.gz +0 -0
  125. data/upgrades/1.1.0.tar.gz +0 -0
  126. data/upgrades/1.6.0.tar.gz +0 -0
  127. data/upgrades/1.6.1.tar.gz +0 -0
  128. data/upgrades/2.0.0.tar.gz +0 -0
  129. data/yard_templates/api_doc/doc_index/html/body.erb +3 -0
  130. data/yard_templates/api_doc/doc_index/setup.rb +8 -0
  131. data/yard_templates/api_doc/html/css/api-doc.css +222 -0
  132. data/yard_templates/api_doc/html/favicon.ico +0 -0
  133. data/yard_templates/api_doc/html/js/switch_tab.js +17 -0
  134. data/yard_templates/api_doc/html/setup.rb +59 -0
  135. data/yard_templates/api_doc/layout/html/footer.erb +3 -0
  136. data/yard_templates/api_doc/layout/html/header.erb +7 -0
  137. data/yard_templates/api_doc/layout/html/layout.erb +13 -0
  138. data/yard_templates/api_doc/layout/setup.rb +24 -0
  139. data/yard_templates/api_doc/profile/html/attributes.erb +10 -0
  140. data/yard_templates/api_doc/profile/html/profile.erb +6 -0
  141. data/yard_templates/api_doc/profile/html/relations.erb +10 -0
  142. data/yard_templates/api_doc/profile/setup.rb +38 -0
  143. data/yard_templates/api_doc/profile_attribute/html/attribute.erb +23 -0
  144. data/yard_templates/api_doc/profile_attribute/setup.rb +21 -0
  145. data/yard_templates/api_doc/profile_relation/html/relation.erb +37 -0
  146. data/yard_templates/api_doc/profile_relation/setup.rb +41 -0
  147. data/yard_templates/api_doc/resource/html/attributes.erb +10 -0
  148. data/yard_templates/api_doc/resource/html/profile.erb +14 -0
  149. data/yard_templates/api_doc/resource/html/relations.erb +10 -0
  150. data/yard_templates/api_doc/resource/html/resource.erb +5 -0
  151. data/yard_templates/api_doc/resource/setup.rb +56 -0
  152. data/yard_templates/api_doc/resource_attribute/html/attribute.erb +23 -0
  153. data/yard_templates/api_doc/resource_attribute/setup.rb +20 -0
  154. data/yard_templates/api_doc/resource_relation/html/relation.erb +47 -0
  155. data/yard_templates/api_doc/resource_relation/setup.rb +80 -0
  156. data/yard_templates/api_doc/setup.rb +31 -0
  157. data/yard_templates/api_doc/sidebar/html/profile_list.erb +8 -0
  158. data/yard_templates/api_doc/sidebar/html/search.erb +7 -0
  159. data/yard_templates/api_doc/sidebar/html/serializer_list.erb +8 -0
  160. data/yard_templates/api_doc/sidebar/html/sidebar.erb +13 -0
  161. data/yard_templates/api_doc/sidebar/setup.rb +56 -0
  162. metadata +140 -30
  163. metadata.gz.sig +1 -2
  164. data/lib/shaf/extensions/current_user.rb +0 -48
  165. data/lib/shaf/responder/hal_serializable.rb +0 -54
@@ -0,0 +1,25 @@
1
+ module Shaf
2
+ module Authenticator
3
+ class BasicAuth < Base
4
+ scheme 'Basic'
5
+
6
+ param :realm
7
+ param :charset, required: false, values: ["UTF-8"]
8
+
9
+ def self.credentials(authorization, _request)
10
+ return unless authorization
11
+
12
+ decoded = String(authorization.unpack("m*").first)
13
+ return {} if decoded.empty?
14
+
15
+ user, password = decoded.split(/:/, 2)
16
+ .map { |str| str unless String(str).empty? }
17
+
18
+ {
19
+ user: user,
20
+ password: password
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaf
4
+ module Authenticator
5
+ class Challenge
6
+ attr_reader :scheme, :parameters, :realm
7
+
8
+ def initialize(scheme, **parameters, &block)
9
+ @scheme = scheme
10
+ @realm = parameters.delete(:realm)&.to_s
11
+ @parameters = parameters
12
+ define_singleton_method(:test, &block)
13
+ end
14
+
15
+ def to_s
16
+ "#{scheme} #{parameter_string}"
17
+ end
18
+
19
+ def realm?(arg)
20
+ realm&.to_s == arg&.to_s
21
+ end
22
+
23
+ private
24
+
25
+ def parameter_string
26
+ params = {}
27
+ params[:realm] = realm if realm
28
+ params.merge(parameters).map { |k,v| %Q(#{k}="#{v}") }.join(', ')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaf
4
+ module Authenticator
5
+ class Parameter
6
+ attr_reader :name, :default, :values
7
+
8
+ def initialize(name, required: true, default: nil, values: nil)
9
+ @name = name
10
+ @required = required
11
+ @default = default
12
+ @values = values&.map(&:downcase)
13
+ end
14
+
15
+ def required?
16
+ @required
17
+ end
18
+
19
+ def optional?
20
+ !required?
21
+ end
22
+
23
+ def valid?(value)
24
+ return optional? if value.nil?
25
+ return true unless values
26
+
27
+ values.include?(value.downcase)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ require 'rack/auth/abstract/request'
2
+
3
+ module Shaf
4
+ module Authenticator
5
+ class Request < Rack::Auth::AbstractRequest
6
+ attr_reader :env
7
+
8
+ def valid?
9
+ !String(authorization).strip.empty?
10
+ end
11
+
12
+ def authorization
13
+ env[authorization_key]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,7 +4,7 @@ module Shaf
4
4
  module Command
5
5
  class Console < Base
6
6
 
7
- identifier %r(\Ac(onsole)?\Z)
7
+ identifier %r(\Ac(\b|onsole)\Z)
8
8
  usage 'console'
9
9
 
10
10
  def call
@@ -1,10 +1,11 @@
1
+ require 'file_transactions'
1
2
  require 'shaf/generator'
2
3
 
3
4
  module Shaf
4
5
  module Command
5
6
  class Generate < Base
6
7
 
7
- identifier %r(\Ag(en(erate)?)?\Z)
8
+ identifier %r(\Ag(\b|en(\b|erate))\Z)
8
9
  usage Generator::Factory.usage.flatten.sort
9
10
 
10
11
  def self.options(parser, options)
@@ -19,7 +20,9 @@ module Shaf
19
20
 
20
21
  def call
21
22
  in_project_root do
22
- Generator::Factory.create(*args, **options).call
23
+ FileTransactions.transaction do
24
+ Generator::Factory.create(*args, **options).call
25
+ end
23
26
  end
24
27
  rescue StandardError => e
25
28
  raise Command::ArgumentError, e.message
@@ -10,21 +10,26 @@ module Shaf
10
10
  usage 'new PROJECT_NAME'
11
11
 
12
12
  def call
13
- @project_name = args.first
14
- if @project_name.nil? || @project_name.empty?
13
+ self.project_name = args.first
14
+ if project_name.nil? || project_name.empty?
15
15
  raise ArgumentError,
16
16
  "Please provide a project name when using command 'new'!"
17
17
  end
18
18
 
19
- create_dir @project_name
20
- Dir.chdir(@project_name) do
19
+ create_dir project_name
20
+ Dir.chdir(project_name) do
21
21
  copy_templates
22
22
  create_gemfile
23
+ create_settings_file
23
24
  write_shaf_version
24
25
  create_ruby_version_file
25
26
  end
26
27
  end
27
28
 
29
+ private
30
+
31
+ attr_accessor :project_name
32
+
28
33
  def create_dir(name)
29
34
  return if Dir.exist? name
30
35
  FileUtils.mkdir_p(name)
@@ -38,9 +43,17 @@ module Shaf
38
43
  File.write "Gemfile", erb(content)
39
44
  end
40
45
 
41
- def erb(content)
42
- return ERB.new(content, 0, '%-<>').result if RUBY_VERSION < "2.6.0"
43
- ERB.new(content, trim_mode: '-<>').result
46
+ def create_settings_file
47
+ settings_file = 'config/settings.yml'
48
+ template_file = File.expand_path("../templates/#{settings_file}.erb", __FILE__)
49
+ content = File.read(template_file)
50
+ File.write settings_file,
51
+ erb(content, project_name: project_name.capitalize)
52
+ end
53
+
54
+ def erb(content, locals = {})
55
+ return ERB.new(content, 0, '%-<>').result_with_hash(locals) if RUBY_VERSION < "2.6.0"
56
+ ERB.new(content, trim_mode: '-<>').result_with_hash(locals)
44
57
  end
45
58
 
46
59
  def copy_templates
@@ -11,6 +11,7 @@ gem 'sinatra-sequel'
11
11
  gem 'bcrypt'
12
12
  gem 'hal_presenter'
13
13
  gem 'redcarpet'
14
+ gem 'yard'
14
15
 
15
16
  group :production, :development do
16
17
  gem 'pg'
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  default: &default
3
+ project_name: <%= project_name %>
3
4
  public_folder: frontend/assets
4
5
  views_folder: frontend/views
5
6
  documents_dir: doc/api
@@ -12,11 +13,6 @@ default: &default
12
13
  hostname: localhost
13
14
  protocol: http
14
15
  port: 3000
15
- auth_token_header: X-Auth-Token
16
- form_profile_name: shaf-form
17
- form_profile_uri: https://gist.githubusercontent.com/sammyhenningsson/39c8aafeaf60192b082762cbf3e08d57/raw/shaf-form.md
18
- error_profile_name: shaf-error
19
- error_profile_uri: https://gist.githubusercontent.com/sammyhenningsson/049d10e2b8978059cde104fc5d6c2d52/raw/shaf-error.md
20
16
 
21
17
  production:
22
18
  <<: *default
data/lib/shaf/errors.rb CHANGED
@@ -64,6 +64,17 @@ module Shaf
64
64
  end
65
65
  end
66
66
 
67
+ class NotAcceptableError < ServerError
68
+ def http_status
69
+ 406
70
+ end
71
+
72
+ def initialize(msg = nil)
73
+ msg ||= 'Resource found, but a suitable representation could not be generated'
74
+ super(msg, code: 'NOT_ACCEPTABLE', title: 'Content negotiation failed')
75
+ end
76
+ end
77
+
67
78
  class ConflictError < ServerError
68
79
  def http_status
69
80
  409
@@ -1,9 +1,9 @@
1
1
  require 'shaf/extensions/log'
2
2
  require 'shaf/extensions/resource_uris'
3
3
  require 'shaf/extensions/controller_hooks'
4
- require 'shaf/extensions/current_user'
5
4
  require 'shaf/extensions/authorize'
6
5
  require 'shaf/extensions/symbolic_routes'
6
+ require 'shaf/extensions/api_routes'
7
7
 
8
8
  module Shaf
9
9
  def self.extensions
@@ -11,9 +11,9 @@ module Shaf
11
11
  Log,
12
12
  ResourceUris,
13
13
  ControllerHooks,
14
- CurrentUser,
15
14
  Authorize,
16
- SymbolicRoutes
15
+ SymbolicRoutes,
16
+ ApiRoutes # This extension must be registered after `SymbolicRoutes`!
17
17
  ]
18
18
  end
19
19
  end
@@ -0,0 +1,60 @@
1
+ require 'set'
2
+
3
+ module Shaf
4
+ module ApiRoutes
5
+ class Registry
6
+ class << self
7
+ def register(controller, method, symbol)
8
+ routes[controller][symbol] << method.to_s.upcase
9
+ end
10
+
11
+ def controllers
12
+ routes.keys.sort_by(&:to_s)
13
+ end
14
+
15
+ def routes_for(controller)
16
+ sorted = routes[controller].keys.sort_by(&:to_s)
17
+ sorted.each do |symbol|
18
+ yield route_info(controller, symbol)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def routes
25
+ @routes ||= Hash.new do |hash, key|
26
+ # Group routes with conditionals together (`Set.new`). Like:
27
+ # get(:foobar_path, agent: /ios/) { "ios specific" }
28
+ # get(:foobar_path, agent: /android/) { "android specific" }
29
+ hash[key] = Hash.new { |h, k| h[k] = Set.new }
30
+ end
31
+ end
32
+
33
+ def route_info(controller, symbol)
34
+ methods = routes[controller][symbol].to_a
35
+ template_method = :"#{symbol}_template"
36
+
37
+ if controller.respond_to? template_method
38
+ template = controller.public_send(template_method)
39
+ else
40
+ template = symbol
41
+ symbol = '-'
42
+ end
43
+
44
+ [methods, template, symbol]
45
+ end
46
+ end
47
+ end
48
+
49
+ Shaf::SUPPORTED_HTTP_METHODS.each do |method|
50
+ define_method method do |path, **options, &block|
51
+ path_str = path.to_s
52
+ path_str.sub!(/_uri/, '_path')
53
+ path_str = "#{path_str}_path" unless path_str.end_with? '_path'
54
+ path_str.sub!(/_path/, '_collection_path') if options[:collection]
55
+ Registry.register(self, method, path_str.to_sym)
56
+ super(path, **options, &block)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -8,22 +8,25 @@ module Shaf
8
8
 
9
9
  attr_reader :policy_class
10
10
 
11
- def authorize_with(policy_class)
12
- @policy_class = policy_class
13
- end
14
-
15
11
  def self.registered(app)
16
12
  app.helpers Helpers
17
13
  end
14
+
15
+ def authorize_with(policy_class)
16
+ @policy_class = policy_class
17
+ end
18
18
  end
19
19
 
20
20
  module Helpers
21
21
  def authorize(action, resource = nil)
22
- policy(resource) or raise Authorize::NoPolicyError
22
+ policy = policy(resource)
23
+ raise Authorize::NoPolicyError unless policy
24
+
23
25
  method = __method_for(action)
24
- return @policy.public_send(method) if @policy.respond_to? method
26
+ return policy.public_send(method) if policy.respond_to? method
27
+
25
28
  raise Authorize::MissingPolicyAction,
26
- "#{@policy.class} does not implement method #{method}"
29
+ "#{policy.class} does not implement method #{method}"
27
30
  end
28
31
 
29
32
  def authorize!(action, resource = nil)
@@ -33,9 +36,8 @@ module Shaf
33
36
  private
34
37
 
35
38
  def policy(resource)
36
- return @policy if defined?(@policy) && @policy
37
39
  user = current_user if respond_to? :current_user
38
- @policy = self.class.policy_class&.new(user, resource)
40
+ self.class.policy_class&.new(user, resource)
39
41
  end
40
42
 
41
43
  def __method_for(action)
@@ -7,7 +7,7 @@ module Shaf
7
7
  end
8
8
 
9
9
  def log
10
- $logger ||= Logger.new('/dev/nul')
10
+ Shaf.log
11
11
  end
12
12
  end
13
13
 
@@ -11,15 +11,25 @@ module Shaf
11
11
  end
12
12
  end
13
13
 
14
+ class << self
15
+ def resource_uris_for(name, **kwargs)
16
+ CreateUriMethods.new(name, **kwargs).call
17
+ end
18
+
19
+ def register_uri(name, uri)
20
+ MethodBuilder.new(name, uri).call
21
+ end
22
+ end
23
+
14
24
  def resource_uris_for(name, **kwargs)
15
- result = CreateUriMethods.new(name, **kwargs).call
25
+ result = ResourceUris.resource_uris_for(name, **kwargs)
16
26
  UriHelperMethods.add_path_helpers(self, result)
17
27
 
18
28
  include UriHelper unless self < UriHelper
19
29
  end
20
30
 
21
31
  def register_uri(name, uri)
22
- result = MethodBuilder.new(name, uri).call
32
+ result = ResourceUris.register_uri(name, uri)
23
33
  UriHelperMethods.add_path_helpers(self, result)
24
34
 
25
35
  include UriHelper unless self < UriHelper
@@ -39,23 +49,23 @@ module Shaf
39
49
  end
40
50
 
41
51
  def add_path_helpers(clazz, methods)
42
- @path_helpers ||= {}
43
- @path_helpers[clazz] ||= []
44
- @path_helpers[clazz].concat Array(methods)
52
+ path_helpers[clazz].concat Array(methods)
45
53
  end
46
54
 
47
55
  def path_helpers_for(clazz = nil)
48
- @path_helpers ||= {}
49
- return @path_helpers unless clazz
50
- return [] unless @path_helpers.key?(clazz)
51
- @path_helpers[clazz] ||= []
56
+ return path_helpers unless clazz
57
+ path_helpers[clazz]
58
+ end
59
+
60
+ def path_helpers
61
+ @path_helpers ||= Hash.new { |hash, key| hash[key] = [] }
52
62
  end
53
63
 
54
64
  # For cleaning up after tests
55
65
  def remove_all
56
66
  helpers = instance_methods - [:path_helpers]
57
67
  remove_method(*helpers)
58
- @path_helpers = {}
68
+ @path_helpers = Hash.new { |hash, key| hash[key] = [] }
59
69
  end
60
70
  end
61
71
 
@@ -107,11 +117,15 @@ module Shaf
107
117
 
108
118
  def call
109
119
  if plural_name == name
120
+ # Deprecated code path
121
+ # Remove this branch and only keep the `else` behavior when dropping
122
+ # support for this
110
123
  register_resource_helper_by_arg
111
124
  else
112
125
  register_collection_helper
113
126
  register_resource_helper
114
127
  end
128
+
115
129
  register_new_resource_helper
116
130
  register_edit_resource_helper
117
131
  @added_path_methods
@@ -125,7 +139,9 @@ module Shaf
125
139
  return if skip? :collection
126
140
 
127
141
  template_uri = "#{base}/#{plural_name}".freeze
128
- register(plural_name, template_uri)
142
+ method_name = plural_name
143
+ method_name = "#{name}_collection" if name == @plural_name
144
+ register(method_name, template_uri)
129
145
  end
130
146
 
131
147
  def register_resource_helper
@@ -140,7 +156,8 @@ module Shaf
140
156
  # as argument and the resources uri when no arguments are provided.
141
157
  def register_resource_helper_by_arg
142
158
  return register_resource_helper if skip? :collection
143
- return register_collection_helper if skip? :new
159
+ register_collection_helper
160
+ return if skip? :new
144
161
 
145
162
  resource_template_uri = "#{base}/#{plural_name}/:id"
146
163
  collection_template_uri = "#{base}/#{plural_name}"
@@ -180,24 +197,33 @@ module Shaf
180
197
  end
181
198
 
182
199
  class MethodBuilder
200
+ NO_GIVEN_VALUE = Object.new
201
+
183
202
  def self.query_string(query)
184
- return "" unless query.any?
185
- "?#{query.map { |key,value| "#{key}=#{value}" }.join("&")}"
203
+ return '' unless query&.any?
204
+
205
+ fragment_id = query.delete(:fragment_id)
206
+ fragment_str = "##{fragment_id}" if fragment_id
207
+
208
+ query_str = query.map { |a| a.join('=') }.join('&')
209
+ query_str = "?#{query_str}" unless query_str.empty?
210
+
211
+ [query_str, fragment_str].join
186
212
  end
187
213
 
188
214
  def initialize(name, uri, alt_uri: nil)
189
215
  @name = name
190
- @uri = uri
191
- @alt_uri = alt_uri
216
+ @uri = uri.dup.freeze
217
+ @alt_uri = alt_uri.dup.freeze
192
218
  end
193
219
 
194
220
  def call
195
221
  if UriHelper.respond_to? uri_method_name
196
222
  exception = ResourceUris::UriHelperMethodAlreadyExistError
197
- raise exception.new(@name, uri_method_name)
223
+ raise exception.new(name, uri_method_name)
198
224
  end
199
225
 
200
- if @alt_uri.nil?
226
+ if alt_uri.nil?
201
227
  build_methods
202
228
  else
203
229
  build_methods_with_optional_arg
@@ -206,6 +232,8 @@ module Shaf
206
232
 
207
233
  private
208
234
 
235
+ attr_reader :name, :uri, :alt_uri
236
+
209
237
  def build_methods
210
238
  UriHelperMethods.eval_method uri_method_string
211
239
  UriHelperMethods.eval_method path_method_string
@@ -225,11 +253,11 @@ module Shaf
225
253
  end
226
254
 
227
255
  def uri_method_name
228
- "#{@name}_uri".freeze
256
+ "#{name}_uri".freeze
229
257
  end
230
258
 
231
259
  def path_method_name
232
- "#{@name}_path".freeze
260
+ "#{name}_path".freeze
233
261
  end
234
262
 
235
263
  def path_matcher_name
@@ -244,81 +272,92 @@ module Shaf
244
272
  "#{uri_method_name}_template".freeze
245
273
  end
246
274
 
247
- def uri_signature(optional_args: 0)
248
- signature(uri_method_name, optional_args: optional_args)
275
+ def uri_signature(uri: @uri, optional_args: 0)
276
+ signature(uri_method_name, uri, optional_args: optional_args)
249
277
  end
250
278
 
251
- def path_signature(optional_args: 0)
252
- signature(path_method_name, optional_args: optional_args)
279
+ def path_signature(uri: @uri, optional_args: 0)
280
+ signature(path_method_name, uri, optional_args: optional_args)
253
281
  end
254
282
 
255
- def signature(method_name, optional_args: 0)
256
- s = "#{method_name}("
283
+ def signature(method_name, uri, optional_args: 0)
284
+ args = extract_symbols(uri).size.times.map { |i| "arg#{i}" }
285
+ sym_count = args.size
257
286
 
258
- symbols = extract_symbols.size.times.map { |i| "arg#{i}" }
259
- sym_count = symbols.size
287
+ optional_args.times { |i| args << "arg#{sym_count + i} = nil" }
288
+ args << '**query'
260
289
 
261
- args = []
262
- symbols.each_with_index do |arg, i|
263
- if i < sym_count - optional_args
264
- args << "arg#{i}"
265
- else
266
- args << "arg#{i} = nil"
267
- end
268
- end
269
- s << (args.empty? ? "**query)" : "#{args.join(', ')}, **query)")
290
+ "#{method_name}(#{args.join(', ')})"
270
291
  end
271
292
 
272
293
  def uri_method_string
273
294
  base_uri = UriHelper.base_uri
274
- <<~Ruby
295
+ <<~RUBY
275
296
  def #{uri_signature}
276
297
  query_str = Shaf::MethodBuilder.query_string(query)
277
- \"#{base_uri}#{interpolated_uri_string(@uri)}\#{query_str}\".freeze
298
+ \"#{base_uri}#{interpolated_uri_string(uri)}\#{query_str}\".freeze
278
299
  end
279
- Ruby
300
+ RUBY
280
301
  end
281
302
 
282
303
  def path_method_string
283
- <<~Ruby
304
+ <<~RUBY
284
305
  def #{path_signature}
285
306
  query_str = Shaf::MethodBuilder.query_string(query)
286
- \"#{interpolated_uri_string(@uri)}\#{query_str}\".freeze
307
+ \"#{interpolated_uri_string(uri)}\#{query_str}\".freeze
287
308
  end
288
- Ruby
309
+ RUBY
289
310
  end
290
311
 
291
312
  def uri_method_with_optional_arg_string
292
313
  base_uri = UriHelper.base_uri
293
- arg_no = extract_symbols.size - 1
294
- <<~Ruby
295
- def #{uri_signature(optional_args: 1)}
314
+ arg_no = extract_symbols(alt_uri).size
315
+ <<~RUBY
316
+ def #{uri_signature(uri: alt_uri, optional_args: 1)}
296
317
  query_str = Shaf::MethodBuilder.query_string(query)
297
318
  if arg#{arg_no}.nil?
298
- \"#{base_uri}#{interpolated_uri_string(@alt_uri)}\#{query_str}\".freeze
319
+ warn <<~DEPRECATION
320
+
321
+ Deprecated use of collection uri helper:
322
+ To get the collection uri use ##{name}_collection_uri instead of ##{uri_method_name}.
323
+ Or pass an argument to ##{uri_method_name} to get the uri to a resource.
324
+ \#{caller.find { |s| !s.match? %r{lib/shaf/extensions/resource_uris.rb} }}
325
+
326
+ DEPRECATION
327
+
328
+ \"#{base_uri}#{interpolated_uri_string(alt_uri)}\#{query_str}\".freeze
299
329
  else
300
- \"#{base_uri}#{interpolated_uri_string(@uri)}\#{query_str}\".freeze
330
+ \"#{base_uri}#{interpolated_uri_string(uri)}\#{query_str}\".freeze
301
331
  end
302
332
  end
303
- Ruby
333
+ RUBY
304
334
  end
305
335
 
306
336
  def path_method_with_optional_arg_string
307
- arg_no = extract_symbols.size - 1
308
- <<~Ruby
309
- def #{path_signature(optional_args: 1)}
337
+ arg_no = extract_symbols(alt_uri).size
338
+ <<~RUBY
339
+ def #{path_signature(uri: alt_uri, optional_args: 1)}
310
340
  query_str = Shaf::MethodBuilder.query_string(query)
311
341
  if arg#{arg_no}.nil?
312
- \"#{interpolated_uri_string(@alt_uri)}\#{query_str}\".freeze
342
+ warn <<~DEPRECATION
343
+
344
+ Deprecated use of collection path helper:
345
+ To get the collection path use ##{name}_collection_path instead of ##{path_method_name}.
346
+ Or pass an argument to ##{path_method_name} to get the path to a resource.
347
+ \#{caller.find { |s| !s.match? %r{lib/shaf/extensions} }}
348
+
349
+ DEPRECATION
350
+
351
+ \"#{interpolated_uri_string(alt_uri)}\#{query_str}\".freeze
313
352
  else
314
- \"#{interpolated_uri_string(@uri)}\#{query_str}\".freeze
353
+ \"#{interpolated_uri_string(uri)}\#{query_str}\".freeze
315
354
  end
316
355
  end
317
- Ruby
356
+ RUBY
318
357
  end
319
358
 
320
359
  def extract_symbols(uri = @uri)
321
- uri.split('/').grep(/:.*/).map { |t| t[1..-1].to_sym }
360
+ uri.split('/').grep(/\A:.+/).map { |t| t[1..-1].to_sym }
322
361
  end
323
362
 
324
363
  def transform_symbols(uri)
@@ -334,8 +373,13 @@ module Shaf
334
373
  return uri if uri == '/'
335
374
 
336
375
  transform_symbols(uri) do |segment, i|
337
- sym = segment[1..-1]
338
- "\#{arg#{i}.respond_to?(#{segment}) ? arg#{i}.#{sym} : arg#{i}}"
376
+ # if the uri is templated (starting with a '{'), then we need to
377
+ # exclude it from the interpolated string but add it back to the end of
378
+ # the segment.
379
+ last = (segment.index('{') || 0) - 1
380
+ sym = segment[1..last]
381
+ template = segment[(last + 1)..-1] unless last == -1
382
+ "\#{arg#{i}.respond_to?(:#{sym}) ? arg#{i}.#{sym} : arg#{i}}#{template}"
339
383
  end
340
384
  end
341
385
 
@@ -343,23 +387,55 @@ module Shaf
343
387
  uri, alt_uri = @uri, @alt_uri
344
388
 
345
389
  if alt_uri.nil?
346
- ->(_ = nil) { uri.freeze }
390
+ -> { uri }
347
391
  else
348
- ->(collection = false) { collection ? alt_uri : uri }
392
+ deprecated_method = template_method_name
393
+ replacing_method = "#{name}_collection_path_template"
394
+
395
+ lambda do |collection = NO_GIVEN_VALUE|
396
+ if collection != NO_GIVEN_VALUE
397
+ warn <<~DEPRECATION
398
+
399
+ Deprecated use of uri template helper with `collection` argument:
400
+ Use #{replacing_method} instead of #{deprecated_method}"
401
+ #{caller.find { |s| !s.match? %r{lib/shaf/extensions} }}
402
+
403
+ DEPRECATION
404
+ else
405
+ collection = false
406
+ end
407
+
408
+ collection ? alt_uri : uri
409
+ end
349
410
  end
350
411
  end
351
412
 
352
413
  def path_mather_patterns
353
414
  [
354
- @uri.gsub(%r{:[^/]*}, '\w+'),
355
- @alt_uri&.gsub(%r{:[^/]*}, '\w+')
415
+ uri.gsub(%r{:[^/]*}, '\w+'),
416
+ alt_uri&.gsub(%r{:[^/]*}, '\w+')
356
417
  ].compact.map { |str| Regexp.new("\\A#{str}\\Z") }
357
418
  end
358
419
 
359
420
  def path_matcher_proc
360
421
  patterns = path_mather_patterns
361
422
 
362
- lambda do |path = nil, collection: false|
423
+ deprecated_method = path_matcher_name
424
+ replacing_method = "#{name}_collection_path?"
425
+
426
+ lambda do |path = nil, collection: NO_GIVEN_VALUE|
427
+ if collection != NO_GIVEN_VALUE
428
+ warn <<~DEPRECATION
429
+
430
+ Deprecated use of uri predicate helper with `collection` argument:
431
+ Use #{replacing_method} instead of #{deprecated_method}(collection: true)
432
+ #{caller.find { |s| !s.match? %r{lib/shaf/extensions} }}
433
+
434
+ DEPRECATION
435
+ else
436
+ collection = false
437
+ end
438
+
363
439
  unless path
364
440
  r = request if respond_to? :request
365
441
  path = r.path_info if r&.respond_to? :path_info