shapeable 0.3.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +69 -22
- data/lib/shapeable.rb +22 -34
- data/lib/shapeable/configuration.rb +24 -0
- data/lib/shapeable/errors.rb +22 -0
- data/lib/shapeable/extenders.rb +16 -0
- data/lib/shapeable/shape.rb +67 -0
- data/lib/shapeable/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6950ec9d5f7b64533f0f5eb8a6b0cc8b9f20bd3
|
4
|
+
data.tar.gz: edc0ef5b43d1a8e3ddd1277ae000bfff82cb9407
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5330318952bc212e50be07ac8a31defb8362b54dc595ba9f89bf5854444d473bfb79ba981680990c525af0394752f6eb5ebf12052132370988abbef5224f8da
|
7
|
+
data.tar.gz: 2532a733031bfe3f58c77a85bc761fe8fed6a961c2affeecc5a2bc4e681df830d89469cb15db6b853701c6c3fde58893580e00f2e917663c900b8a2682687071
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -5,7 +5,7 @@ API versioning that promotes convention over configuration.
|
|
5
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.
|
6
6
|
|
7
7
|
|
8
|
-
##
|
8
|
+
## Example
|
9
9
|
|
10
10
|
|
11
11
|
Let's say we have a resource of type `Foo` and `foos_controller.rb` that includes our gem and has a `show` action:
|
@@ -21,43 +21,90 @@ class FoosController < ActionController::Base
|
|
21
21
|
end
|
22
22
|
```
|
23
23
|
|
24
|
-
|
24
|
+
Assume we have the following `ActiveModelSerializer` directory structure:
|
25
|
+
|
26
|
+
```
|
27
|
+
app
|
28
|
+
serializers
|
29
|
+
foo
|
30
|
+
v1
|
31
|
+
foo_short_serializer.rb
|
32
|
+
foo_full_serializer.rb
|
33
|
+
v2
|
34
|
+
foo_full_serializer.rb
|
35
|
+
foo_serializer.rb
|
36
|
+
```
|
25
37
|
|
38
|
+
Inside our controller we can reference the `shape` method, which returns the serializer constant. The top level module path in which the serializer can be found must be specified using the `path` option on either the `acts_as_shapeable`, or the `shape` method. In this case it would be `Serializers::Foo`. If the path is not specified either in `acts_as_shapeable` or `shape`, then calling `shape` will raise an `ArgumentError`.
|
39
|
+
|
40
|
+
Now say we have several clients, each expecting a different serialized response.
|
41
|
+
|
42
|
+
A client on v1 would like a list of `foos` in short form:
|
26
43
|
`curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1 shape=short'`
|
27
44
|
|
28
|
-
|
45
|
+
When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V1::FooShortSerializer`
|
46
|
+
|
29
47
|
|
48
|
+
A client on v2 would like a list of `foos` in short form:
|
30
49
|
`curl http://localhost:3000/foos -H 'Accept: application/javascript; version=2 shape=short'`
|
31
50
|
|
32
|
-
|
51
|
+
When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V2::FooShortSerializer`
|
52
|
+
|
33
53
|
|
54
|
+
A client on v1 would like a list of `foos` in full form:
|
34
55
|
`curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1 shape=full'`
|
35
56
|
|
36
|
-
|
57
|
+
When we reference `shape` inside the controller, it returns the following constant: `Serializers::Foo::V1::FooFullSerializer`
|
37
58
|
|
59
|
+
|
60
|
+
## Configuring Defaults
|
61
|
+
|
62
|
+
Both `acts_as_shapeable` and `shape` accept the following arguments:
|
63
|
+
|
64
|
+
* `default_version`: The default version in cases where the header is not specified.
|
65
|
+
* `default_shape`: The deafault shape in cases where the shape is not specified.
|
66
|
+
|
67
|
+
The options defined on `shape` have greater precedence over those defined on `acts_as_shapeable`.
|
68
|
+
|
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`.
|
70
|
+
|
71
|
+
Using the same example controller and directory structure from above, a client sends no headers:
|
38
72
|
`curl http://localhost:3000/foos -H 'Accept: application/javascript;`
|
73
|
+
Shapeable uses the defaults provided, and resolves `shape` to the following constant: `Serializers::Foo::V1::FooShortSerializer`
|
39
74
|
|
40
|
-
Assuming we have the following ActiveModelSerializer directory structure, we wouldn't have to change the above controller at all to fulfill these requests:
|
41
75
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
76
|
+
Shapeable can also be configured with a global configuration file. All options which can be passed to `acts_as_shapeable` and `shape` can also be configured using this method. Configuration options inside the config file have the lowest precedence.
|
77
|
+
|
78
|
+
You can configure shapeable by passing in all configuration options(`default_version`, `default_shape`, and `path`), to `Shapeable.configure` in block format. One approach is to put this in a config file that lives at `config/shapeable.rb`.
|
79
|
+
|
80
|
+
```Ruby
|
81
|
+
# config/shapeable.rb
|
82
|
+
Shapeable.configure do |config|
|
83
|
+
config.default_version = 1
|
84
|
+
config.default_shape = 'default'
|
85
|
+
config.path = Serializers::Bar
|
86
|
+
end
|
52
87
|
```
|
53
88
|
|
89
|
+
## Enforce Versioning and Shaping
|
54
90
|
|
55
|
-
|
91
|
+
There are a few additional options which allow you to decide whether you want to enforce version or shape.
|
56
92
|
|
57
|
-
|
93
|
+
`enforce_versioning`, and `enforce_shape` can be passed to `Shapeable.configure`. By default both are set to true.
|
94
|
+
|
95
|
+
When `enforce_versioning` is set to false, version will be ignored, and the version module will not be prepended. So the following request
|
96
|
+
|
97
|
+
`curl http://localhost:3000/foos -H 'Accept: application/javascript; shape=default'`
|
98
|
+
|
99
|
+
Will be constructed as `Serializers::Foo::FooDefaultSerializer` without the version module prepended.
|
100
|
+
|
101
|
+
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
|
+
|
103
|
+
`curl http://localhost:3000/foos -H 'Accept: application/javascript; version=1'`
|
104
|
+
|
105
|
+
Will be constructed as `Serializers::Foo::V1::FooSerializer`.
|
58
106
|
|
59
|
-
|
60
|
-
* `default_version`: The default version in cases where the header is not specified
|
61
|
-
* `default_shape`: The deafault shape in cases where the shape is not specified
|
107
|
+
## Gotchas
|
62
108
|
|
63
|
-
|
109
|
+
* Shapeable infers the resource name from the path. Specifically it expects that the last constant in the path is the resource. So in the case of `acts_as_shapeable(path: Serializers::Foo)` Shapeable assumes that the resource is `Foo`.
|
110
|
+
* If we want a shape of `FooSerializer` we must specify a default_shape of `''` (empty string).
|
data/lib/shapeable.rb
CHANGED
@@ -1,49 +1,37 @@
|
|
1
1
|
require 'shapeable/version'
|
2
|
+
require 'shapeable/configuration'
|
3
|
+
require 'shapeable/shape'
|
4
|
+
require 'shapeable/errors'
|
5
|
+
require 'shapeable/extenders'
|
2
6
|
|
3
7
|
module Shapeable
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
version = version_str.nil? ? default_version : version_str.to_i
|
20
|
-
shape = request.accept[/shape\s?=\s?(\w+)/, 1] || default_shape
|
21
|
-
raise UnresolvedShapeError, "Unable to resolve shape. Try specifying a default version and shape" unless version && shape
|
22
|
-
begin
|
23
|
-
serializer = path.const_get("V#{version}").const_get("#{resource}#{shape.camelize}Serializer")
|
24
|
-
rescue NameError
|
25
|
-
raise InvalidShapeError, "Invalid shape. Tried to find version #{version} with shape #{shape}"
|
8
|
+
CONFIG_FILE = './config/shapeable.rb'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration_data
|
12
|
+
|
13
|
+
def configure(file = nil)
|
14
|
+
configuration
|
15
|
+
if block_given?
|
16
|
+
yield(configuration)
|
17
|
+
else
|
18
|
+
if File.exists?(CONFIG_FILE)
|
19
|
+
file ||= CONFIG_FILE
|
20
|
+
require file
|
21
|
+
else
|
22
|
+
raise ArgumentError, "Configure requires a block or the existance of a #{CONFIG_FILE} in your project"
|
26
23
|
end
|
27
24
|
end
|
28
|
-
serializer
|
29
25
|
end
|
30
|
-
end
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
def normalize_shapeable_options(opts)
|
35
|
-
opts.keep_if do |k, _v|
|
36
|
-
[:path, :default_shape, :default_version].include?(k)
|
27
|
+
def configuration
|
28
|
+
self.configuration_data ||= Shapeable::Configuration.new
|
37
29
|
end
|
38
30
|
end
|
39
|
-
|
40
|
-
class InvalidShapeError < NameError; end
|
41
|
-
class UnresolvedShapeError < Exception; end
|
42
|
-
|
43
31
|
end
|
44
32
|
|
45
33
|
if defined? ActionController::Base
|
46
34
|
ActionController::Base.class_eval do
|
47
|
-
extend Shapeable
|
35
|
+
extend Shapeable::Extenders
|
48
36
|
end
|
49
37
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Shapeable
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :path, :default_version, :default_shape, :enforce_versioning, :enforce_shape
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@path = nil
|
8
|
+
@default_version = nil
|
9
|
+
@default_shape = nil
|
10
|
+
@enforce_versioning = true
|
11
|
+
@enforce_shape = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def as_json
|
15
|
+
{
|
16
|
+
path: path,
|
17
|
+
default_version: default_version,
|
18
|
+
default_shape: default_shape,
|
19
|
+
enforce_versioning: enforce_versioning,
|
20
|
+
enforce_shape: enforce_shape
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Shapeable
|
2
|
+
module Errors
|
3
|
+
class InvalidShapeError < NameError
|
4
|
+
def initialize(path, shape, version: nil)
|
5
|
+
resource = path.name.split('::').last.constantize
|
6
|
+
if version
|
7
|
+
super("Invalid shape #{path}::V#{version}::#{resource}#{shape.camelize}Serializer")
|
8
|
+
else
|
9
|
+
super("Invalid shape #{path}::#{resource}#{shape.camelize}Serializer")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class UnresolvedShapeError < Exception
|
15
|
+
def initialize(msg = 'Unable to resolve shape.' \
|
16
|
+
' Try specifying a default version and shape.'
|
17
|
+
)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'errors'
|
2
|
+
module Shapeable
|
3
|
+
module Shape
|
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
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
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)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge_shapeable_options(*opts_array)
|
35
|
+
opts_array.each_with_object({}) do |val, obj|
|
36
|
+
obj.merge!(val)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
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
|
43
|
+
end
|
44
|
+
|
45
|
+
def resolve_shape(accept_header, default)
|
46
|
+
accept_header[/shape\s?=\s?(\w+)/, 1] || default
|
47
|
+
end
|
48
|
+
|
49
|
+
def construct_constant(path, shape: nil, version: nil)
|
50
|
+
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")
|
55
|
+
else
|
56
|
+
return path.const_get("#{resource}Serializer") unless version
|
57
|
+
return path_with_version.const_get("#{resource}Serializer")
|
58
|
+
end
|
59
|
+
rescue NameError
|
60
|
+
raise Shapeable::Errors::InvalidShapeError.new(path, shape, version: version)
|
61
|
+
end
|
62
|
+
|
63
|
+
def infer_resource_name(path)
|
64
|
+
path.name.split('::').last.constantize
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/shapeable/version.rb
CHANGED
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.6.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-
|
12
|
+
date: 2016-04-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: actionpack
|
@@ -87,6 +87,10 @@ files:
|
|
87
87
|
- README.md
|
88
88
|
- circle.yml
|
89
89
|
- lib/shapeable.rb
|
90
|
+
- lib/shapeable/configuration.rb
|
91
|
+
- lib/shapeable/errors.rb
|
92
|
+
- lib/shapeable/extenders.rb
|
93
|
+
- lib/shapeable/shape.rb
|
90
94
|
- lib/shapeable/version.rb
|
91
95
|
- shapeable.gemspec
|
92
96
|
homepage: https://github.com/viewthespace/shapeable
|