shapeable 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f6950ec9d5f7b64533f0f5eb8a6b0cc8b9f20bd3
4
- data.tar.gz: edc0ef5b43d1a8e3ddd1277ae000bfff82cb9407
3
+ metadata.gz: a7d39aa43ffe8a1b7c7aa7dfe557982057468b14
4
+ data.tar.gz: 54dd027ea85753aac5541856fcce80455aa5828a
5
5
  SHA512:
6
- metadata.gz: d5330318952bc212e50be07ac8a31defb8362b54dc595ba9f89bf5854444d473bfb79ba981680990c525af0394752f6eb5ebf12052132370988abbef5224f8da
7
- data.tar.gz: 2532a733031bfe3f58c77a85bc761fe8fed6a961c2affeecc5a2bc4e681df830d89469cb15db6b853701c6c3fde58893580e00f2e917663c900b8a2682687071
6
+ metadata.gz: c68b5fcb8fedd59d00150e174277025fc6b40c5fa62663530cd504a70989e0250f4cda480451924cc2d7106acbb5cf1e5062f8434dc42d229be596bce2c7f1ae
7
+ data.tar.gz: bb086e17c28795076e00d7b00d343b85bafccf747e4426e21ba46835371dca87f941780e18c61507f277a131706ea4b0a4d169ccbb63d0f65d6d5517fde2bc18
data/.gitignore CHANGED
@@ -7,4 +7,5 @@
7
7
  /doc/
8
8
  /pkg/
9
9
  /spec/reports/
10
- /tmp/
10
+ /tmp/
11
+ *.swp
data/.pryrc ADDED
@@ -0,0 +1,6 @@
1
+ if defined?(PryDebugger)
2
+ Pry.commands.alias_command 'c', 'continue'
3
+ Pry.commands.alias_command 's', 'step'
4
+ Pry.commands.alias_command 'n', 'next'
5
+ Pry.commands.alias_command 'f', 'finish'
6
+ end
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  API versioning that promotes convention over configuration.
4
4
 
5
- The premise of this gem is that consumers of your API need versioning and different shapes of your resources. Without proper thought into versioning and shaping, your codebase can quickly resolve into a redundant and confusing state. This gem tries to solve that problem by allowing the API owner to use simple conventions -- Accept headers and ActiveModelSerializer namespacing -- to achieve controller reuse by controllers delegating resource versioning and shaping to the serializer level.
5
+ The premise of this gem is that consumers of your API need versioning and different shapes of your resources. Without proper thought into versioning and shaping, your codebase can quickly resolve into a redundant and confusing state. This gem tries to solve that problem by allowing the API owner to use simple conventions -- Accept headers and ActiveModelSerializer namespacing -- to achieve controller reuse by controllers delegating resource versioning and shaping to the consumer.
6
6
 
7
7
 
8
8
  ## Example
@@ -39,37 +39,59 @@ Inside our controller we can reference the `shape` method, which returns the ser
39
39
 
40
40
  Now say we have several clients, each expecting a different serialized response.
41
41
 
42
+ ---
43
+
42
44
  A client on v1 would like a list of `foos` in short form:
43
- `curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1 shape=short'`
45
+
46
+ `curl http://localhost:3000/foos -H 'Accept: application/json; version=1 shape=short'`
47
+
48
+ OR
49
+
50
+ `curl http://localhost:3000/foos?version=1&shape=short`
44
51
 
45
52
  When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V1::FooShortSerializer`
46
53
 
54
+ ---
47
55
 
48
56
  A client on v2 would like a list of `foos` in short form:
49
- `curl http://localhost:3000/foos -H 'Accept: application/javascript; version=2 shape=short'`
57
+
58
+ `curl http://localhost:3000/foos -H 'Accept: application/json; version=2 shape=short'`
59
+
60
+ OR
61
+
62
+ `curl http://localhost:3000/foos?version=2&shape=short`
50
63
 
51
64
  When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V2::FooShortSerializer`
52
65
 
66
+ ---
53
67
 
54
68
  A client on v1 would like a list of `foos` in full form:
55
- `curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1 shape=full'`
69
+
70
+ `curl http://localhost:3000/foos -H 'Accept: application/json; version=1 shape=full'`
71
+
72
+ OR
73
+
74
+ `curl http://localhost:3000/foos?version=1&shape=full`
56
75
 
57
76
  When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V1::FooFullSerializer`
58
77
 
78
+ ---
79
+
80
+ Note: headers take precedence over query parameters if both are sent
59
81
 
60
82
  ## Configuring Defaults
61
83
 
62
84
  Both `acts_as_shapeable` and `shape` accept the following arguments:
63
85
 
64
- * `default_version`: The default version in cases where the header is not specified.
86
+ * `default_version`: The default version in cases where the version is not specified.
65
87
  * `default_shape`: The deafault shape in cases where the shape is not specified.
66
88
 
67
89
  The options defined on `shape` have greater precedence over those defined on `acts_as_shapeable`.
68
90
 
69
- In cases where the version and/or the shape is not specified in the Accept Header Shapeable will instead use the provided defaults. If no default is provided, and nothing is specified in the header, Shapeable will raise an `UnresolvedShapeError`.
91
+ In cases where the version and/or the shape is not specified in the Accept Header Shapeable will instead use the provided defaults. If no default is provided, and nothing is specified in the header or query params, Shapeable will raise an `UnresolvedShapeError`.
70
92
 
71
93
  Using the same example controller and directory structure from above, a client sends no headers:
72
- `curl http://localhost:3000/foos -H 'Accept: application/javascript;`
94
+ `curl http://localhost:3000/foos -H 'Accept: application/json;`
73
95
  Shapeable uses the defaults provided, and resolves `shape` to the following constant: `Serializers::Foo::V1::FooShortSerializer`
74
96
 
75
97
 
@@ -94,13 +116,13 @@ There are a few additional options which allow you to decide whether you want to
94
116
 
95
117
  When `enforce_versioning` is set to false, version will be ignored, and the version module will not be prepended. So the following request
96
118
 
97
- `curl http://localhost:3000/foos -H 'Accept: application/javascript; shape=default'`
119
+ `curl http://localhost:3000/foos -H 'Accept: application/json; shape=default'`
98
120
 
99
121
  Will be constructed as `Serializers::Foo::FooDefaultSerializer` without the version module prepended.
100
122
 
101
123
  When `enforce_shape` is set to false, shape will be optional. When shape is not specified, the constant will be constructed with no shape. So the following request:
102
124
 
103
- `curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1'`
125
+ `curl http://localhost:3000/foos -H 'Accept: application/json; version=1'`
104
126
 
105
127
  Will be constructed as `Serializers::Foo::V1::FooSerializer`.
106
128
 
@@ -1,7 +1,13 @@
1
1
  module Shapeable
2
2
  class Configuration
3
3
 
4
- attr_accessor :path, :default_version, :default_shape, :enforce_versioning, :enforce_shape
4
+ attr_accessor :path,
5
+ :default_version,
6
+ :default_shape,
7
+ :enforce_versioning,
8
+ :enforce_shape,
9
+ :shape_attr_override,
10
+ :version_attr_override
5
11
 
6
12
  def initialize
7
13
  @path = nil
@@ -9,6 +15,8 @@ module Shapeable
9
15
  @default_shape = nil
10
16
  @enforce_versioning = true
11
17
  @enforce_shape = true
18
+ @shape_attr_override = nil
19
+ @version_attr_override = nil
12
20
  end
13
21
 
14
22
  def as_json
@@ -17,7 +25,9 @@ module Shapeable
17
25
  default_version: default_version,
18
26
  default_shape: default_shape,
19
27
  enforce_versioning: enforce_versioning,
20
- enforce_shape: enforce_shape
28
+ enforce_shape: enforce_shape,
29
+ shape_attr_override: shape_attr_override,
30
+ version_attr_override: version_attr_override
21
31
  }
22
32
  end
23
33
  end
@@ -12,9 +12,19 @@ module Shapeable
12
12
  end
13
13
 
14
14
  class UnresolvedShapeError < Exception
15
- def initialize(msg = 'Unable to resolve shape.' \
16
- ' Try specifying a default version and shape.'
17
- )
15
+ def initialize(msg = 'Unable to resolve shape. Try specifying a default shape.')
16
+ super
17
+ end
18
+ end
19
+
20
+ class UnresolvedVersionError < Exception
21
+ def initialize(msg = 'Unable to resolve version. Try specifying a default version.')
22
+ super
23
+ end
24
+ end
25
+
26
+ class UnresolvedPathError < Exception
27
+ def initialize(msg = 'Unable to resolve path. Try specifying a path.')
18
28
  super
19
29
  end
20
30
  end
@@ -2,13 +2,8 @@ module Shapeable
2
2
  module Extenders
3
3
 
4
4
  def acts_as_shapeable(**opts)
5
- require_relative 'shape'
6
- include Shapeable::Shape
7
-
8
- class_eval do
9
- define_method(:acts_as_shapeable_opts) do
10
- opts
11
- end
5
+ define_method(:acts_as_shapeable_opts) do
6
+ opts
12
7
  end
13
8
  end
14
9
 
@@ -2,59 +2,100 @@ require_relative 'errors'
2
2
  module Shapeable
3
3
  module Shape
4
4
 
5
- def self.included base
6
- base.class_eval do
7
- def shape(shape_opts = {})
8
- return unless request.accept
9
- opts = merge_shapeable_options(
10
- Shapeable.configuration.as_json, acts_as_shapeable_opts, shape_opts
11
- )
12
- normalize_shapeable_options(opts)
13
- raise ArgumentError, 'Specify a path' unless opts[:path]
14
- shape = resolve_shape(request.accept, opts[:default_shape])
15
- if opts[:enforce_versioning]
16
- version = resolve_version(request.accept, opts[:default_version])
17
- raise Shapeable::Errors::UnresolvedShapeError unless version && shape if opts[:enforce_shape]
18
- constant = construct_constant(opts[:path], shape: shape, version: version)
19
- else
20
- raise Shapeable::Errors::UnresolvedShapeError unless shape if opts[:enforce_shape]
21
- constant = construct_constant(opts[:path], shape: shape)
22
- end
23
- constant
24
- end
5
+ def self.included(klass)
6
+ klass.extend Shapeable::Extenders
7
+ end
8
+
9
+ def shape(default_shape_opts = {})
10
+ opts = merge_shapeable_options(
11
+ Shapeable.configuration.as_json,
12
+ acts_as_shapeable_opts,
13
+ default_shape_opts,
14
+ request_shape_options(
15
+ request,
16
+ Shapeable.configuration.as_json[:shape_attr_override] || 'shape',
17
+ Shapeable.configuration.as_json[:version_attr_override] || 'version',
18
+ )
19
+ )
20
+ normalize_shapeable_options!(opts)
21
+ validate_and_resolve_shape(opts)
22
+ end
23
+
24
+ def merge_shapeable_options(*opts)
25
+ opts.each_with_object({}) do |val, obj|
26
+ obj.merge!(val)
25
27
  end
26
28
  end
27
29
 
28
- def normalize_shapeable_options(opts)
29
- opts.keep_if do |k, _v|
30
- [:path, :default_shape, :default_version, :enforce_versioning, :enforce_shape].include?(k)
30
+ def normalize_shapeable_options!(opts)
31
+ opts.keep_if do |k, _|
32
+ [
33
+ :path,
34
+ :default_shape,
35
+ :default_version,
36
+ :shape,
37
+ :version,
38
+ :enforce_versioning,
39
+ :enforce_shape
40
+ ].include?(k)
31
41
  end
32
42
  end
33
43
 
34
- def merge_shapeable_options(*opts_array)
35
- opts_array.each_with_object({}) do |val, obj|
36
- obj.merge!(val)
44
+ def validate_and_resolve_shape(opts)
45
+ version = opts[:version] || opts[:default_version]
46
+ shape = opts[:shape] || opts[:default_shape]
47
+
48
+ if opts[:path].blank?
49
+ raise Shapeable::Errors::UnresolvedPathError
50
+ elsif opts[:enforce_versioning] && version.blank?
51
+ raise Shapeable::Errors::UnresolvedVersionError
52
+ elsif opts[:enforce_shape] && shape.blank?
53
+ raise Shapeable::Errors::UnresolvedShapeError
37
54
  end
55
+
56
+ if opts[:enforce_versioning]
57
+ construct_constant(opts[:path], shape: shape, version: version)
58
+ else
59
+ construct_constant(opts[:path], shape: shape)
60
+ end
61
+ end
62
+
63
+ def request_shape_options(request, shape_attr_name, version_attr_name)
64
+ {
65
+ version: resolve_header_version(request.accept, version_attr_name) || resolve_params_version(request.params, version_attr_name),
66
+ shape: resolve_header_shape(request.accept, shape_attr_name) || resolve_params_shape(request.params, shape_attr_name)
67
+ }.delete_if { |_, v| v.blank? }
68
+ end
69
+
70
+ def resolve_header_version(accept_header, version_attr_name)
71
+ if accept_header
72
+ version_str = accept_header[/#{version_attr_name}\s?=\s?(\d+)/, 1]
73
+ version_str.nil? ? nil : version_str.to_i
74
+ end
75
+ end
76
+
77
+ def resolve_header_shape(accept_header, shape_attr_name)
78
+ accept_header[/#{shape_attr_name}\s?=\s?(\w+)/, 1] if accept_header
38
79
  end
39
80
 
40
- def resolve_version(accept_header, default)
41
- version_str = accept_header[/version\s?=\s?(\d+)/, 1]
42
- version_str.nil? ? default : version_str.to_i
81
+ def resolve_params_version(params, version_attr_name)
82
+ params[version_attr_name]
43
83
  end
44
84
 
45
- def resolve_shape(accept_header, default)
46
- accept_header[/shape\s?=\s?(\w+)/, 1] || default
85
+ def resolve_params_shape(params, shape_attr_name)
86
+ params[shape_attr_name]
47
87
  end
48
88
 
49
89
  def construct_constant(path, shape: nil, version: nil)
50
90
  resource = infer_resource_name(path)
51
- if shape
52
- return path.const_get("#{resource}#{shape.camelize}Serializer") unless version
53
- path_with_version = path.const_get("V#{version}")
54
- return path_with_version.const_get("#{resource}#{shape.camelize}Serializer")
91
+ if shape && version
92
+ path.const_get("V#{version}::#{resource}#{shape.camelize}Serializer")
93
+ elsif shape
94
+ path.const_get("#{resource}#{shape.camelize}Serializer")
95
+ elsif version
96
+ path.const_get("V#{version}::#{resource}Serializer")
55
97
  else
56
- return path.const_get("#{resource}Serializer") unless version
57
- return path_with_version.const_get("#{resource}Serializer")
98
+ path.const_get("#{resource}Serializer")
58
99
  end
59
100
  rescue NameError
60
101
  raise Shapeable::Errors::InvalidShapeError.new(path, shape, version: version)
@@ -1,3 +1,3 @@
1
1
  module Shapeable
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
data/lib/shapeable.rb CHANGED
@@ -30,8 +30,4 @@ module Shapeable
30
30
  end
31
31
  end
32
32
 
33
- if defined? ActionController::Base
34
- ActionController::Base.class_eval do
35
- extend Shapeable::Extenders
36
- end
37
- end
33
+ ActionController::Base.include Shapeable::Shape if defined? ActionController::Base
data/shapeable.gemspec CHANGED
@@ -29,4 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'activesupport'
30
30
  spec.add_development_dependency 'active_model_serializers'
31
31
  spec.add_development_dependency 'rspec-rails'
32
+ spec.add_development_dependency 'pry'
33
+ spec.add_development_dependency 'pry-byebug'
32
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shapeable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shawn O'Mara
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-04-29 00:00:00.000000000 Z
12
+ date: 2017-07-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -67,6 +67,34 @@ dependencies:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pry
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: pry-byebug
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
70
98
  description: |-
71
99
  The premise of this gem is that consumers of your API need versioning
72
100
  and different shapes of your resources. Without proper thought into versioning
@@ -82,6 +110,7 @@ extensions: []
82
110
  extra_rdoc_files: []
83
111
  files:
84
112
  - ".gitignore"
113
+ - ".pryrc"
85
114
  - Gemfile
86
115
  - LICENSE.md
87
116
  - README.md
@@ -113,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
142
  version: '0'
114
143
  requirements: []
115
144
  rubyforge_project:
116
- rubygems_version: 2.4.8
145
+ rubygems_version: 2.6.11
117
146
  signing_key:
118
147
  specification_version: 4
119
148
  summary: API versioning that promotes convention over configuration