versioncake 4.0.0 → 4.1.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +33 -0
  3. data/Appraisals +9 -3
  4. data/CHANGELOG.md +42 -1
  5. data/Gemfile.lock +31 -29
  6. data/README.md +13 -11
  7. data/SECURITY.md +9 -0
  8. data/gemfiles/rails5.0.gemfile.lock +77 -72
  9. data/gemfiles/rails5.2.gemfile.lock +66 -62
  10. data/gemfiles/rails6.0.gemfile +3 -3
  11. data/gemfiles/rails6.0.gemfile.lock +73 -67
  12. data/gemfiles/rails7.0.gemfile +9 -0
  13. data/gemfiles/rails7.0.gemfile.lock +127 -0
  14. data/lib/versioncake/controller_additions.rb +0 -6
  15. data/lib/versioncake/railtie.rb +9 -0
  16. data/lib/versioncake/response_strategy/http_content_type_strategy.rb +4 -2
  17. data/lib/versioncake/strategies/extraction_strategy.rb +11 -7
  18. data/lib/versioncake/version.rb +1 -1
  19. data/lib/versioncake/version_checker.rb +2 -1
  20. data/lib/versioncake/version_context_service.rb +1 -1
  21. data/lib/versioncake/versioned_request.rb +1 -1
  22. data/lib/versioncake/view_additions.rb +8 -88
  23. data/lib/versioncake/view_additions_rails5.rb +70 -0
  24. data/lib/versioncake/view_additions_rails6.rb +69 -0
  25. data/lib/versioncake/view_additions_rails7.rb +155 -0
  26. data/lib/versioncake.rb +3 -1
  27. data/spec/integration/controller/unversioned_controller_spec.rb +1 -1
  28. data/spec/integration/view/render_spec.rb +8 -1
  29. data/spec/integration/view/view_additions_rails5_spec.rb +67 -0
  30. data/spec/integration/view/view_additions_rails6_spec.rb +44 -0
  31. data/spec/integration/view/view_additions_rails7_spec.rb +76 -0
  32. data/spec/unit/strategies/extraction_strategy_spec.rb +2 -2
  33. data/spec/unit/version_checker_spec.rb +1 -1
  34. data/spec/unit/versioned_request_spec.rb +0 -7
  35. data/versioncake.gemspec +5 -5
  36. metadata +29 -20
  37. data/.travis.yml +0 -20
  38. data/spec/integration/view/view_additions_spec.rb +0 -42
@@ -0,0 +1,69 @@
1
+ require 'action_view'
2
+
3
+ # register an addition detail for the lookup context to understand,
4
+ # this will allow us to have the versions available upon lookup in
5
+ # the resolver.
6
+ ActionView::LookupContext.register_detail(:versions){ [] }
7
+
8
+ ActionView::PathResolver.class_eval do
9
+ ActionView::PathResolver::EXTENSIONS.replace({
10
+ locale: ".",
11
+ formats: ".",
12
+ versions: ".",
13
+ variants: "+",
14
+ handlers: "."
15
+ })
16
+ Kernel::silence_warnings {
17
+ ActionView::PathResolver::DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:versions,}{+:variants,}{.:handlers,}"
18
+ }
19
+
20
+ # The default extract handler expects that the handler is the last extension and
21
+ # the format is the next one. Since we are replacing the DEFAULT_PATTERN, we need to
22
+ # make sure that we extract the format from the correct position.
23
+ #
24
+ # The version may be stuck inbetween the format and the handler. This is actually pretty tricky
25
+ # because the version is optional and the locale is optional-which means when there are 3 'pieces'
26
+ # it may be the locale, format and handler or the format, version and handler. To check this, we will
27
+ # try one additional time if there are more pieces, which should cover all the cases:
28
+ #
29
+ # Cases:
30
+ # 1: assume version is in the extension, pieces = ['html','erb']
31
+ # 2: assume version is in the extension, pieces = ['html','v1','erb']
32
+ # 3: assume version is in the extension, pieces = ['en','html','erb']
33
+ # 4: assume version is in the extension, pieces = ['en','html','v1','erb']
34
+ #
35
+ def extract_handler_and_format(path, default_formats)
36
+ pieces = File.basename(path).split(".")
37
+ pieces.shift
38
+
39
+ extension = pieces.pop
40
+ handler = ActionView::Template.handler_for_extension(extension)
41
+ format = get_format_from_pieces(pieces, Mime)
42
+
43
+ [handler, format]
44
+ end
45
+
46
+ # If there are still pieces and we didn't find a valid format, we may
47
+ # have a version in the extension, so try one more time to pop the format.
48
+ def get_format_from_pieces(pieces, format_list)
49
+ format = nil
50
+ pieces.reverse.each do |piece|
51
+ if ActionView::PathResolver::EXTENSIONS.is_a?(Hash) &&
52
+ ActionView::PathResolver::EXTENSIONS.include?(:variants)
53
+ piece = piece.split(ActionView::PathResolver::EXTENSIONS[:variants], 2).first # remove variant from format
54
+ end
55
+
56
+ format = format_list[piece]
57
+ break unless format.nil?
58
+ end
59
+ format
60
+ end
61
+ end
62
+
63
+ ActionView::Template.class_eval do
64
+ # the identifier method name filters out numbers,
65
+ # but we want to preserve them for v1 etc.
66
+ def identifier_method_name #:nodoc:
67
+ inspect.gsub(/[^a-z0-9_]/, '_')
68
+ end
69
+ end
@@ -0,0 +1,155 @@
1
+ require 'action_view'
2
+
3
+ # register an addition detail for the lookup context to understand,
4
+ # this will allow us to have the versions available upon lookup in
5
+ # the resolver.
6
+ ActionView::LookupContext.register_detail(:versions){ [] }
7
+
8
+ ActionView::Resolver::PathParser.class_eval do
9
+ def build_path_regex
10
+ handlers = ActionView::Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
11
+ formats = ActionView::Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
12
+ locales = "[a-z]{2}(?:-[A-Z]{2})?"
13
+ variants = "[^.]*"
14
+
15
+ %r{
16
+ \A
17
+ (?:(?<prefix>.*)/)?
18
+ (?<partial>_)?
19
+ (?<action>.*?)
20
+ (?:\.(?<locale>#{locales}))??
21
+ (?:\.(?<format>#{formats}))??
22
+ (?:\+(?<variant>#{variants}))??
23
+ (?:\.(?<versions>v[0-9]+))??
24
+ (?:\.(?<handler>#{handlers}))?
25
+ \z
26
+ }x
27
+ end
28
+
29
+ def parse(path)
30
+ @regex ||= build_path_regex
31
+ match = @regex.match(path)
32
+ path = ActionView::TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
33
+ details = ActionView::TemplateDetails.new(
34
+ match[:locale]&.to_sym,
35
+ match[:handler]&.to_sym,
36
+ match[:format]&.to_sym,
37
+ match[:variant]&.to_sym,
38
+ match[:versions]&.to_sym,
39
+ )
40
+
41
+ ActionView::Resolver::PathParser::ParsedPath.new(path, details)
42
+ end
43
+ end
44
+
45
+ ActionView::TemplateDetails::Requested.class_eval do
46
+ attr_reader :locale, :handlers, :formats, :variants, :versions
47
+ attr_reader :locale_idx, :handlers_idx, :formats_idx, :variants_idx, :versions_idx
48
+
49
+ def initialize(locale:, handlers:, formats:, variants:, versions:)
50
+ @locale = locale
51
+ @handlers = handlers
52
+ @formats = formats
53
+ @variants = variants
54
+ @versions = versions
55
+
56
+ @locale_idx = build_idx_hash(locale)
57
+ @handlers_idx = build_idx_hash(handlers)
58
+ @formats_idx = build_idx_hash(formats)
59
+ @versions_idx = build_idx_hash(versions)
60
+ if variants == :any
61
+ @variants_idx = ANY_HASH
62
+ else
63
+ @variants_idx = build_idx_hash(variants)
64
+ end
65
+ end
66
+ end
67
+
68
+ ActionView::TemplateDetails.class_eval do
69
+ attr_reader :locale, :handler, :format, :variant, :version
70
+
71
+ def initialize(locale, handler, format, variant, version)
72
+ @locale = locale
73
+ @handler = handler
74
+ @format = format
75
+ @variant = variant
76
+ @version = version
77
+ end
78
+
79
+ def matches?(requested)
80
+ requested.formats_idx[@format] &&
81
+ requested.locale_idx[@locale] &&
82
+ requested.versions_idx[@version] &&
83
+ requested.variants_idx[@variant] &&
84
+ requested.handlers_idx[@handler]
85
+ end
86
+
87
+ def sort_key_for(requested)
88
+ [
89
+ requested.formats_idx[@format],
90
+ requested.locale_idx[@locale],
91
+ requested.versions_idx[@version],
92
+ requested.variants_idx[@variant],
93
+ requested.handlers_idx[@handler]
94
+ ]
95
+ end
96
+ end
97
+
98
+ ActionView::Template.class_eval do
99
+ def initialize(source, identifier, handler, locals:, format: nil, variant: nil, virtual_path: nil, version: nil)
100
+ @source = source
101
+ @identifier = identifier
102
+ @handler = handler
103
+ @compiled = false
104
+ @locals = locals
105
+ @virtual_path = virtual_path
106
+
107
+ @variable = if @virtual_path
108
+ base = @virtual_path.end_with?("/") ? "" : ::File.basename(@virtual_path)
109
+ base =~ /\A_?(.*?)(?:\.\w+)*\z/
110
+ $1.to_sym
111
+ end
112
+
113
+ @format = format
114
+ @version = version
115
+ @variant = variant
116
+ @compile_mutex = Mutex.new
117
+ end
118
+
119
+ def marshal_dump # :nodoc:
120
+ [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant, @version ]
121
+ end
122
+
123
+ def marshal_load(array) # :nodoc:
124
+ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant, @version = *array
125
+ @compile_mutex = Mutex.new
126
+ end
127
+
128
+ # the identifier method name filters out numbers,
129
+ # but we want to preserve them for v1 etc.
130
+ # TODO: Consider updating to match current implementation:
131
+ # short_identifier.tr("^a-z_", "_")
132
+ def identifier_method_name #:nodoc:
133
+ short_identifier.gsub(/[^a-z0-9_]/, '_')
134
+ end
135
+ end
136
+
137
+ ActionView::UnboundTemplate.class_eval do
138
+ delegate :version, to: :@details
139
+
140
+ def build_template(locals)
141
+ ActionView::Template.new(
142
+ @source,
143
+ @identifier,
144
+ details.handler_class,
145
+
146
+ format: details.format_or_default,
147
+ variant: variant&.to_s,
148
+ virtual_path: @virtual_path,
149
+ version: version&.to_s,
150
+
151
+ locals: locals.map(&:to_s)
152
+ )
153
+ end
154
+ end
155
+
data/lib/versioncake.rb CHANGED
@@ -26,6 +26,7 @@ if defined?(Rails)
26
26
  require 'versioncake/controller_additions'
27
27
  require 'versioncake/view_additions'
28
28
  require 'versioncake/engine'
29
+ require 'versioncake/railtie'
29
30
  end
30
31
 
31
32
  module VersionCake
@@ -38,4 +39,5 @@ module VersionCake
38
39
  def self.setup
39
40
  yield self.config
40
41
  end
41
- end
42
+ end
43
+
@@ -6,7 +6,7 @@ describe UnversionedController, type: :controller do
6
6
  context '#index' do
7
7
  render_views
8
8
 
9
- it { expect(response).to be_success }
9
+ it { expect(response).to be_successful }
10
10
  it { expect(response.body).to eq 'unversioned index' }
11
11
  end
12
12
  end
@@ -3,7 +3,14 @@ require './spec/rails_helper'
3
3
  describe ActionView::Base do
4
4
  let(:path) { ActionView::FileSystemResolver.new('./spec/fixtures') }
5
5
  let(:view_paths) { ActionView::PathSet.new([path]) }
6
- let(:view) { ActionView::Base.new(view_paths) }
6
+ let(:view) do
7
+ clazz = if ActionPack::VERSION::MAJOR >= 7
8
+ ActionView::Base.with_empty_template_cache
9
+ else
10
+ ActionView::Base
11
+ end
12
+ clazz.new(ActionView::LookupContext.new(view_paths), [], nil)
13
+ end
7
14
  let(:version_override) { nil }
8
15
  subject { view.render(template: 'templates/versioned', versions: version_override) }
9
16
 
@@ -0,0 +1,67 @@
1
+ require './spec/rails_helper'
2
+
3
+ if ActionPack::VERSION::MAJOR == 5
4
+ describe ActionView::PathResolver do
5
+ let(:resolver) { ActionView::PathResolver.new }
6
+
7
+ context '#extract_handler_and_format_and_variant' do
8
+ subject do
9
+ resolver.extract_handler_and_format_and_variant("application.#{extension}")
10
+ end
11
+
12
+ let(:variant) { subject[2].to_s }
13
+ let(:format) { subject[1].to_s }
14
+ let(:handler) { subject[0] }
15
+
16
+ context 'when only handler and format are present' do
17
+ let(:extension) { 'html.erb' }
18
+
19
+ it do
20
+ expect(format).to eq 'text/html'
21
+ expect(variant).to be_empty
22
+ expect(handler).to be_a ActionView::Template::Handlers::ERB
23
+ end
24
+ end
25
+
26
+ context 'when handler, format and version are present' do
27
+ let(:extension) { 'json.v1.jbuilder' }
28
+
29
+ it do
30
+ expect(format).to eq 'application/json'
31
+ expect(variant).to be_empty
32
+ expect(handler).to be_a ActionView::Template::Handlers::Raw
33
+ end
34
+ end
35
+
36
+ context 'when handler, format and locale are present' do
37
+ let(:extension) { 'en.json.jbuilder' }
38
+
39
+ it do
40
+ expect(format).to eq 'application/json'
41
+ expect(variant).to be_empty
42
+ expect(handler).to be_a ActionView::Template::Handlers::Raw
43
+ end
44
+ end
45
+
46
+ context 'when handler, format, locale and version are present' do
47
+ let(:extension) { 'en.json.v1.jbuilder' }
48
+
49
+ it do
50
+ expect(format).to eq 'application/json'
51
+ expect(variant).to be_empty
52
+ expect(handler).to be_a ActionView::Template::Handlers::Raw
53
+ end
54
+ end
55
+
56
+ context 'when handler, format, variant and version are present' do
57
+ let(:extension) { 'json+tablet.v1.jbuilder' }
58
+
59
+ it do
60
+ expect(format).to eq 'application/json'
61
+ expect(variant).to eq 'tablet'
62
+ expect(handler).to be_a ActionView::Template::Handlers::Raw
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ require './spec/rails_helper'
2
+
3
+ if ActionPack::VERSION::MAJOR == 6
4
+ describe ActionView::PathResolver do
5
+ let(:resolver) { ActionView::PathResolver.new }
6
+
7
+ context '#extract_handler_and_format' do
8
+ subject(:template_format) do
9
+ _, format = resolver.extract_handler_and_format("application.#{template_extension}", nil)
10
+ format.to_s
11
+ end
12
+
13
+ context 'when only handler and format are present' do
14
+ let(:template_extension) { 'html.erb' }
15
+
16
+ it { expect(template_format).to eq 'text/html' }
17
+ end
18
+
19
+ context 'when handler, format and version are present' do
20
+ let(:template_extension) { 'json.v1.jbuilder' }
21
+
22
+ it { expect(template_format).to eq 'application/json' }
23
+ end
24
+
25
+ context 'when handler, format and locale are present' do
26
+ let(:template_extension) { 'en.json.jbuilder' }
27
+
28
+ it { expect(template_format).to eq 'application/json' }
29
+ end
30
+
31
+ context 'when handler, format, locale and version are present' do
32
+ let(:template_extension) { 'en.json.v1.jbuilder' }
33
+
34
+ it { expect(template_format).to eq 'application/json' }
35
+ end
36
+
37
+ context 'when handler, format, variant and version are present' do
38
+ let(:template_extension) { 'application.json+tablet.v1.jbuilder' }
39
+
40
+ it { expect(template_format).to eq 'application/json' }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ require './spec/rails_helper'
2
+
3
+ if ActionPack::VERSION::MAJOR == 7
4
+ describe ActionView::Resolver::PathParser do
5
+ let(:resolver) { ActionView::Resolver::PathParser.new }
6
+
7
+ context '#extract_handler_and_format' do
8
+ subject(:parsed_path) do
9
+ resolver.parse("application.#{template_extension}")
10
+ end
11
+
12
+ context 'when only handler and format are present' do
13
+ let(:template_extension) { 'html.erb' }
14
+
15
+ it do
16
+ expect(parsed_path.details.format).to eq :html
17
+ expect(parsed_path.details.handler).to eq :erb
18
+ end
19
+ end
20
+
21
+ context 'when handler, format and version are present' do
22
+ let(:template_extension) { 'json.v1.builder' }
23
+
24
+ it do
25
+ expect(parsed_path.details.format).to eq :json
26
+ expect(parsed_path.details.handler).to eq :builder
27
+ expect(parsed_path.details.version).to eq :v1
28
+ end
29
+ end
30
+
31
+ context 'when handler, format and locale are present' do
32
+ let(:template_extension) { 'en.json.builder' }
33
+
34
+ it do
35
+ expect(parsed_path.details.locale).to eq :en
36
+ expect(parsed_path.details.format).to eq :json
37
+ expect(parsed_path.details.handler).to eq :builder
38
+ expect(parsed_path.details.version).to eq nil
39
+ end
40
+ end
41
+
42
+ context 'when handler, format, locale and version are present' do
43
+ let(:template_extension) { 'en.json.v1.builder' }
44
+
45
+ it do
46
+ expect(parsed_path.details.format).to eq :json
47
+ expect(parsed_path.details.handler).to eq :builder
48
+ expect(parsed_path.details.version).to eq :v1
49
+ end
50
+ end
51
+
52
+ context 'when handler, format, variant and version are present' do
53
+ let(:template_extension) { 'json+tablet.v1.builder' }
54
+
55
+ it do
56
+ expect(parsed_path.details.variant).to eq :tablet
57
+ expect(parsed_path.details.format).to eq :json
58
+ expect(parsed_path.details.handler).to eq :builder
59
+ expect(parsed_path.details.version).to eq :v1
60
+ end
61
+ end
62
+
63
+ context 'when handler, format, variant, locale and version are present' do
64
+ let(:template_extension) { 'en.json+tablet.v1.builder' }
65
+
66
+ it do
67
+ expect(parsed_path.details.locale).to eq :en
68
+ expect(parsed_path.details.variant).to eq :tablet
69
+ expect(parsed_path.details.format).to eq :json
70
+ expect(parsed_path.details.handler).to eq :builder
71
+ expect(parsed_path.details.version).to eq :v1
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -60,13 +60,13 @@ describe VersionCake::ExtractionStrategy do
60
60
  it "it fails to create a custom strategy for a proc with no parameters" do
61
61
  expect do
62
62
  VersionCake::ExtractionStrategy.lookup(lambda{})
63
- end.to raise_error(Exception)
63
+ end.to raise_error(VersionCake::ExtractionStrategy::InvalidStrategyError)
64
64
  end
65
65
 
66
66
  it "it raises error when no strategy is found" do
67
67
  expect do
68
68
  VersionCake::ExtractionStrategy.lookup(:fake_extraction)
69
- end.to raise_error(Exception)
69
+ end.to raise_error(VersionCake::ExtractionStrategy::InvalidStrategyError)
70
70
  end
71
71
 
72
72
  describe '.list' do
@@ -10,7 +10,7 @@ describe VersionCake::VersionChecker do
10
10
  end
11
11
  let(:version) { }
12
12
  subject do
13
- checker = VersionCake::VersionChecker.new(resource, version)
13
+ checker = VersionCake::VersionChecker.new(version, resource)
14
14
  checker.execute
15
15
  end
16
16
 
@@ -24,12 +24,5 @@ describe VersionCake::VersionedRequest do
24
24
  it { expect(versioned_request.version).to be_nil }
25
25
  it { expect(versioned_request.failed).to be_falsey }
26
26
  end
27
-
28
- context 'with a strategy failure' do
29
- let(:strategies) { [ lambda { raise 'Failed extraction' } ] }
30
-
31
- it { expect(versioned_request.version).to be_nil }
32
- it { expect(versioned_request.failed).to be_truthy }
33
- end
34
27
  end
35
28
  end
data/versioncake.gemspec CHANGED
@@ -19,14 +19,14 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.required_ruby_version = '>= 1.9.2'
21
21
 
22
- s.add_dependency('actionpack', '~> 5.0')
23
- s.add_dependency('activesupport', '~> 5.0')
24
- s.add_dependency('railties', '~> 5.0')
25
- s.add_dependency('tzinfo', '~> 1.2')
22
+ s.add_dependency('actionpack', '> 5.0')
23
+ s.add_dependency('activesupport', '> 5.0')
24
+ s.add_dependency('railties', '> 5.0')
25
+ s.add_dependency('tzinfo', '> 1.2')
26
26
 
27
27
  s.add_development_dependency 'appraisal', '~> 2.2'
28
28
  s.add_development_dependency 'coveralls', '~> 0.8'
29
- s.add_development_dependency 'rake', '~> 13.0'
29
+ s.add_development_dependency 'rake', '> 12.0'
30
30
 
31
31
  s.add_development_dependency 'rspec', '~> 3.6'
32
32
  s.add_development_dependency 'rspec-rails', '~> 3.6'