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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ed1e8139b673601f51a5a4a95830670945710d4
4
- data.tar.gz: 013a597aba452cc63d267909298c84e7ee027449
3
+ metadata.gz: f6950ec9d5f7b64533f0f5eb8a6b0cc8b9f20bd3
4
+ data.tar.gz: edc0ef5b43d1a8e3ddd1277ae000bfff82cb9407
5
5
  SHA512:
6
- metadata.gz: 7e3babc9c59f8fb1ee8a8e9a6825d83ff29f6afc93ef9ef546a526170e2cd728cd33d3b3bd52ce4136294eb62701486d9fad02ba5330e9bda3f19be8c3ba5b54
7
- data.tar.gz: 4990746ce0a05398bf8ab340147fc1158fee3a92c5215d85bcfb1f659a6ed452e646f89235f1bf55617d732bd3f58b1771c3f61b0cc8a36777f829ae9ddab661
6
+ metadata.gz: d5330318952bc212e50be07ac8a31defb8362b54dc595ba9f89bf5854444d473bfb79ba981680990c525af0394752f6eb5ebf12052132370988abbef5224f8da
7
+ data.tar.gz: 2532a733031bfe3f58c77a85bc761fe8fed6a961c2affeecc5a2bc4e681df830d89469cb15db6b853701c6c3fde58893580e00f2e917663c900b8a2682687071
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec
3
+ gemspec
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
- ## example
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
- A client on v1 would like a list of `foos` in short form:
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
- A client on v2 would like a list of `foos` in short form:
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
- A client on v1 would like a list of `foos` in full form:
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
- A client on v2 would like a list of `foos` in default form, in this case short:
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
- app
44
- serializers
45
- foo
46
- v1
47
- foo_short_serializer.rb
48
- foo_full_serializer.rb
49
- v2
50
- foo_full_serializer.rb
51
- foo_serializer.rb
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
- The `shape` method returns the serializer constant.
91
+ There are a few additional options which allow you to decide whether you want to enforce version or shape.
56
92
 
57
- Both `acts_as_shapeable` and `shape` accept the following arguments:
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
- * `path`: The top level module where the serializers are defined
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
- All three options must be defined either on `acts_as_shapeable` or `shape`. The options defined on `shape` have greater precedence than those on `acts_as_shapeable`.
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).
@@ -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
- def acts_as_shapeable(**opts)
6
-
7
- normalize_shapeable_options(opts)
8
- acts_as_shapeable_opts = opts || {}
9
-
10
- define_method(:shape) do |opts={}|
11
- opts = acts_as_shapeable_opts.merge(opts)
12
- default_shape = opts[:default_shape]
13
- default_version = opts[:default_version]
14
- path = opts[:path]
15
- raise ArgumentError, "Specify a path" unless path
16
- resource = path.name.split('::').last.constantize
17
- if request.accept
18
- version_str = request.accept[/version\s?=\s?(\d+)/, 1]
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
- private
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,16 @@
1
+ module Shapeable
2
+ module Extenders
3
+
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
12
+ end
13
+ end
14
+
15
+ end
16
+ 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
@@ -1,3 +1,3 @@
1
1
  module Shapeable
2
- VERSION = '0.3.0'
2
+ VERSION = '0.6.0'
3
3
  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.3.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-08 00:00:00.000000000 Z
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