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 +4 -4
- data/.gitignore +2 -1
- data/.pryrc +6 -0
- data/README.md +31 -9
- data/lib/shapeable/configuration.rb +12 -2
- data/lib/shapeable/errors.rb +13 -3
- data/lib/shapeable/extenders.rb +2 -7
- data/lib/shapeable/shape.rb +78 -37
- data/lib/shapeable/version.rb +1 -1
- data/lib/shapeable.rb +1 -5
- data/shapeable.gemspec +2 -0
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7d39aa43ffe8a1b7c7aa7dfe557982057468b14
|
4
|
+
data.tar.gz: 54dd027ea85753aac5541856fcce80455aa5828a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c68b5fcb8fedd59d00150e174277025fc6b40c5fa62663530cd504a70989e0250f4cda480451924cc2d7106acbb5cf1e5062f8434dc42d229be596bce2c7f1ae
|
7
|
+
data.tar.gz: bb086e17c28795076e00d7b00d343b85bafccf747e4426e21ba46835371dca87f941780e18c61507f277a131706ea4b0a4d169ccbb63d0f65d6d5517fde2bc18
|
data/.gitignore
CHANGED
data/.pryrc
ADDED
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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/
|
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/
|
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/
|
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,
|
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
|
data/lib/shapeable/errors.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/shapeable/extenders.rb
CHANGED
@@ -2,13 +2,8 @@ module Shapeable
|
|
2
2
|
module Extenders
|
3
3
|
|
4
4
|
def acts_as_shapeable(**opts)
|
5
|
-
|
6
|
-
|
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
|
|
data/lib/shapeable/shape.rb
CHANGED
@@ -2,59 +2,100 @@ require_relative 'errors'
|
|
2
2
|
module Shapeable
|
3
3
|
module Shape
|
4
4
|
|
5
|
-
def self.included
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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,
|
30
|
-
[
|
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
|
35
|
-
|
36
|
-
|
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
|
41
|
-
|
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
|
46
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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)
|
data/lib/shapeable/version.rb
CHANGED
data/lib/shapeable.rb
CHANGED
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.
|
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:
|
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.
|
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
|