action_figure 0.5.0 → 0.6.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -0
- data/README.md +140 -159
- data/docs/actions.md +68 -60
- data/docs/activesupport-notifications.md +12 -10
- data/docs/configuration.md +16 -2
- data/docs/custom-formatters.md +18 -6
- data/docs/response-formatters.md +159 -3
- data/docs/status-codes.md +35 -0
- data/docs/testing.md +28 -10
- data/docs/validation.md +5 -1
- data/lib/action_figure/core.rb +81 -26
- data/lib/action_figure/format_registry.rb +4 -2
- data/lib/action_figure/formatter.rb +3 -1
- data/lib/action_figure/formatters/default.rb +15 -5
- data/lib/action_figure/formatters/jsend.rb +8 -0
- data/lib/action_figure/formatters/json_api/resource.rb +4 -6
- data/lib/action_figure/formatters/json_api.rb +21 -8
- data/lib/action_figure/formatters/wrapped.rb +8 -0
- data/lib/action_figure/testing/minitest.rb +8 -0
- data/lib/action_figure/testing/rspec.rb +33 -3
- data/lib/action_figure/version.rb +1 -1
- data/lib/action_figure.rb +12 -1
- data/sig/action_figure.rbs +176 -1
- metadata +19 -2
|
@@ -6,13 +6,11 @@ module ActionFigure
|
|
|
6
6
|
# Simple resource serialization
|
|
7
7
|
class Resource
|
|
8
8
|
def self.serialize(resource)
|
|
9
|
-
if resource.
|
|
10
|
-
resource
|
|
11
|
-
elsif resource.respond_to?(:attributes)
|
|
9
|
+
if resource.respond_to?(:attributes)
|
|
12
10
|
serialize_one(resource)
|
|
13
|
-
elsif resource.respond_to?(:each)
|
|
11
|
+
elsif !resource.is_a?(Hash) && resource.respond_to?(:each)
|
|
14
12
|
resource.map { |r| serialize(r) }
|
|
15
|
-
else
|
|
13
|
+
else
|
|
16
14
|
resource
|
|
17
15
|
end
|
|
18
16
|
end
|
|
@@ -20,7 +18,7 @@ module ActionFigure
|
|
|
20
18
|
def self.serialize_one(resource)
|
|
21
19
|
{
|
|
22
20
|
type: resource.class.model_name.element,
|
|
23
|
-
id: resource.id
|
|
21
|
+
id: resource.id&.to_s,
|
|
24
22
|
attributes: resource.attributes.except("id")
|
|
25
23
|
}
|
|
26
24
|
end
|
|
@@ -38,17 +38,30 @@ module ActionFigure
|
|
|
38
38
|
{ json: { errors: convert_errors(errors, "403") }, status: :forbidden }
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def Conflict(errors:)
|
|
42
|
+
{ json: { errors: convert_errors(errors, "409") }, status: :conflict }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def PaymentRequired(errors:)
|
|
46
|
+
{ json: { errors: convert_errors(errors, "402") }, status: :payment_required }
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
private
|
|
42
50
|
|
|
43
|
-
def convert_errors(errors, status)
|
|
51
|
+
def convert_errors(errors, status, prefix = "/data/attributes")
|
|
44
52
|
errors.flat_map do |field, messages|
|
|
45
|
-
pointer = field.to_sym == :base ? "/data" : "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
pointer = field.to_sym == :base ? "/data" : "#{prefix}/#{field}"
|
|
54
|
+
|
|
55
|
+
if messages.is_a?(Hash)
|
|
56
|
+
convert_errors(messages, status, pointer)
|
|
57
|
+
else
|
|
58
|
+
messages.map do |message|
|
|
59
|
+
{
|
|
60
|
+
status: status,
|
|
61
|
+
detail: message,
|
|
62
|
+
source: { pointer: pointer }
|
|
63
|
+
}
|
|
64
|
+
end
|
|
52
65
|
end
|
|
53
66
|
end
|
|
54
67
|
end
|
|
@@ -36,6 +36,14 @@ module ActionFigure
|
|
|
36
36
|
def Forbidden(errors:)
|
|
37
37
|
{ json: { data: nil, errors: errors, status: "error" }, status: :forbidden }
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
def Conflict(errors:)
|
|
41
|
+
{ json: { data: nil, errors: errors, status: "error" }, status: :conflict }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def PaymentRequired(errors:)
|
|
45
|
+
{ json: { data: nil, errors: errors, status: "error" }, status: :payment_required }
|
|
46
|
+
end
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
end
|
|
@@ -38,6 +38,14 @@ module ActionFigure
|
|
|
38
38
|
assert_status(:forbidden, result, msg)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def assert_Conflict(result, msg = nil)
|
|
42
|
+
assert_status(:conflict, result, msg)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def assert_PaymentRequired(result, msg = nil)
|
|
46
|
+
assert_status(:payment_required, result, msg)
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
private
|
|
42
50
|
|
|
43
51
|
def assert_status(expected, result, msg)
|
|
@@ -13,6 +13,7 @@ module ActionFigure
|
|
|
13
13
|
# RSpec.describe Users::Create do
|
|
14
14
|
# it "returns ok" do
|
|
15
15
|
# expect(Users::Create.call(params: ...)).to be_Ok
|
|
16
|
+
# expect(Users::Create.call(params: ...)).to have_action_json(status: "success")
|
|
16
17
|
# end
|
|
17
18
|
# end
|
|
18
19
|
module RSpec
|
|
@@ -23,7 +24,9 @@ module ActionFigure
|
|
|
23
24
|
NoContent: :no_content,
|
|
24
25
|
UnprocessableContent: :unprocessable_content,
|
|
25
26
|
NotFound: :not_found,
|
|
26
|
-
Forbidden: :forbidden
|
|
27
|
+
Forbidden: :forbidden,
|
|
28
|
+
Conflict: :conflict,
|
|
29
|
+
PaymentRequired: :payment_required
|
|
27
30
|
}.freeze
|
|
28
31
|
|
|
29
32
|
MATCHERS.each do |name, status|
|
|
@@ -32,11 +35,38 @@ module ActionFigure
|
|
|
32
35
|
failure_message do |result|
|
|
33
36
|
"expected result status to be #{status.inspect}, but got #{result[:status].inspect}"
|
|
34
37
|
end
|
|
35
|
-
failure_message_when_negated do
|
|
36
|
-
"expected result
|
|
38
|
+
failure_message_when_negated do
|
|
39
|
+
"expected result not to have status #{status.inspect}"
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
43
|
+
|
|
44
|
+
# Asserts against +result[:json]+ using +a_hash_including+ (nested matchers allowed).
|
|
45
|
+
::RSpec::Matchers.define :have_action_json do |expected_fragment|
|
|
46
|
+
include ::RSpec::Matchers
|
|
47
|
+
|
|
48
|
+
match do |result|
|
|
49
|
+
@inner_matcher ||= a_hash_including(expected_fragment)
|
|
50
|
+
next false unless result.is_a?(Hash) && result.key?(:json)
|
|
51
|
+
|
|
52
|
+
@inner_matcher.matches?(result[:json])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
failure_message do |result|
|
|
56
|
+
if !result.is_a?(Hash)
|
|
57
|
+
"expected an ActionFigure result hash, got #{result.inspect}"
|
|
58
|
+
elsif !result.key?(:json)
|
|
59
|
+
"expected #{result.inspect} to include key :json (ActionFigure render hash)"
|
|
60
|
+
else
|
|
61
|
+
"expected result[:json] to #{@inner_matcher.description}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
failure_message_when_negated do |result|
|
|
66
|
+
@inner_matcher ||= a_hash_including(expected_fragment)
|
|
67
|
+
"#{result.inspect} was expected not to match #{@inner_matcher.description}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
40
70
|
end
|
|
41
71
|
end
|
|
42
72
|
end
|
data/lib/action_figure.rb
CHANGED
|
@@ -9,13 +9,24 @@ require_relative "action_figure/formatters/jsend"
|
|
|
9
9
|
require_relative "action_figure/formatters/json_api"
|
|
10
10
|
require_relative "action_figure/formatters/default"
|
|
11
11
|
require_relative "action_figure/formatters/wrapped"
|
|
12
|
-
require "concurrent/map"
|
|
13
12
|
|
|
14
13
|
# ActionFigure provides explicit, purpose-driven operation classes for Rails controller actions.
|
|
15
14
|
module ActionFigure
|
|
16
15
|
extend Configuration
|
|
17
16
|
extend FormatRegistry
|
|
18
17
|
|
|
18
|
+
class IndeterminateEntryPointError < StandardError; end
|
|
19
|
+
|
|
20
|
+
# Backwards-compatible alias for the misspelled constant shipped through 0.6.0.
|
|
21
|
+
# Remove in the next minor release after Unreleased.
|
|
22
|
+
IndeterminantEntryPointError = IndeterminateEntryPointError
|
|
23
|
+
deprecate_constant :IndeterminantEntryPointError
|
|
24
|
+
|
|
25
|
+
# Raised when an action class defines +initialize+. ActionFigure builds instances with
|
|
26
|
+
# +new+ and passes no constructor arguments; use keyword arguments on the entry method
|
|
27
|
+
# or class-level state instead of custom initializers.
|
|
28
|
+
class InitializationNotSupportedError < StandardError; end
|
|
29
|
+
|
|
19
30
|
register_formatter(jsend: Formatters::Jsend)
|
|
20
31
|
register_formatter(jsonapi: Formatters::JsonApi)
|
|
21
32
|
register_formatter(default: Formatters::Default)
|
data/sig/action_figure.rbs
CHANGED
|
@@ -1,4 +1,179 @@
|
|
|
1
|
+
# Type definitions for the ActionFigure gem
|
|
2
|
+
|
|
3
|
+
# Response hash returned by formatter methods
|
|
4
|
+
type ActionFigure::response = { json: Hash[Symbol, untyped], status: Symbol } | { status: Symbol }
|
|
5
|
+
|
|
6
|
+
# Error hash mapping field names to arrays of error messages
|
|
7
|
+
type ActionFigure::error_hash = Hash[Symbol, Array[String]]
|
|
8
|
+
|
|
1
9
|
module ActionFigure
|
|
2
10
|
VERSION: String
|
|
3
|
-
|
|
11
|
+
|
|
12
|
+
class IndeterminateEntryPointError < StandardError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Deprecated alias for IndeterminateEntryPointError (misspelled constant from <= 0.6.0).
|
|
16
|
+
IndeterminantEntryPointError: Class
|
|
17
|
+
|
|
18
|
+
class InitializationNotSupportedError < StandardError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
extend Configuration
|
|
22
|
+
extend FormatRegistry
|
|
23
|
+
|
|
24
|
+
def self.[]: (?Symbol format) -> Module
|
|
25
|
+
def self.included: (Module base) -> void
|
|
26
|
+
def self.register_formatter: (**Module formatters) -> void
|
|
27
|
+
def self.clear_format_module_cache: (Symbol name) -> void
|
|
28
|
+
|
|
29
|
+
# Provides global configuration via ActionFigure.configure
|
|
30
|
+
module Configuration
|
|
31
|
+
class Settings
|
|
32
|
+
attr_accessor format: Symbol
|
|
33
|
+
attr_accessor whiny_extra_params: bool
|
|
34
|
+
attr_accessor api_version: untyped
|
|
35
|
+
attr_accessor activesupport_notifications: bool
|
|
36
|
+
|
|
37
|
+
def initialize: () -> void
|
|
38
|
+
def configure: () { (Settings) -> void } -> void
|
|
39
|
+
def register: (**Module formatters) -> void
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def configure: () { (Settings) -> void } -> void
|
|
43
|
+
def configuration: () -> Settings
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Provides formatter registration and lookup
|
|
47
|
+
module FormatRegistry
|
|
48
|
+
class Formats
|
|
49
|
+
def initialize: () -> void
|
|
50
|
+
def register_formatter: (**Module formatters) -> void
|
|
51
|
+
def fetch: (Symbol name) -> Module
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def register_formatter: (**Module formatters) -> void
|
|
55
|
+
def fetch: (Symbol name) -> Module
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Base module for response formatters
|
|
59
|
+
module Formatter
|
|
60
|
+
REQUIRED_METHODS: Array[Symbol]
|
|
61
|
+
|
|
62
|
+
def NoContent: () -> ActionFigure::response
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Validation pipeline and DSL mixed into action classes
|
|
66
|
+
module Core
|
|
67
|
+
# Cross-parameter rule helpers for dry-validation contracts
|
|
68
|
+
module CrossParamRuleHelpers
|
|
69
|
+
def exclusive_rule: (*Symbol fields, String message) -> void
|
|
70
|
+
def any_rule: (*Symbol fields, String message) -> void
|
|
71
|
+
def one_rule: (*Symbol fields, String message) -> void
|
|
72
|
+
def all_rule: (*Symbol fields, String message) -> void
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Class-level DSL extended into action classes
|
|
76
|
+
module ClassMethods
|
|
77
|
+
def params_schema: () { () -> void } -> void
|
|
78
|
+
def rules: () { () -> void } -> void
|
|
79
|
+
def entry_point: (Symbol name) -> void
|
|
80
|
+
def entry_point_name: () -> Symbol?
|
|
81
|
+
def api_version: (?untyped value) -> untyped
|
|
82
|
+
def contract: () -> untyped
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def entry_point_name: () -> Symbol?
|
|
86
|
+
def contract: () -> untyped
|
|
87
|
+
def validated_call: (**untyped kwargs) -> ActionFigure::response
|
|
88
|
+
|
|
89
|
+
# ActiveSupport::Notifications instrumentation
|
|
90
|
+
module Notifications
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def notify: () { () -> ActionFigure::response } -> ActionFigure::response
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
module Formatters
|
|
98
|
+
# Rails-style responses with { data: } envelope
|
|
99
|
+
module Default
|
|
100
|
+
include ActionFigure::Formatter
|
|
101
|
+
|
|
102
|
+
def Ok: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
103
|
+
def Created: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
104
|
+
def Accepted: (?resource: untyped?, ?meta: untyped?) -> ActionFigure::response
|
|
105
|
+
def UnprocessableContent: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
106
|
+
def NotFound: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
107
|
+
def Forbidden: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
108
|
+
def Conflict: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
109
|
+
def PaymentRequired: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# JSend-formatted responses
|
|
113
|
+
module Jsend
|
|
114
|
+
include ActionFigure::Formatter
|
|
115
|
+
|
|
116
|
+
def Ok: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
117
|
+
def Created: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
118
|
+
def Accepted: (?resource: untyped?, ?meta: untyped?) -> ActionFigure::response
|
|
119
|
+
def UnprocessableContent: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
120
|
+
def NotFound: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
121
|
+
def Forbidden: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
122
|
+
def Conflict: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
123
|
+
def PaymentRequired: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# JSON:API-formatted responses
|
|
127
|
+
module JsonApi
|
|
128
|
+
include ActionFigure::Formatter
|
|
129
|
+
|
|
130
|
+
def Ok: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
131
|
+
def Created: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
132
|
+
def Accepted: (?resource: untyped?, ?meta: untyped?) -> ActionFigure::response
|
|
133
|
+
def UnprocessableContent: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
134
|
+
def NotFound: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
135
|
+
def Forbidden: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
136
|
+
def Conflict: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
137
|
+
def PaymentRequired: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
138
|
+
|
|
139
|
+
# Simple resource serialization for JSON:API
|
|
140
|
+
class Resource
|
|
141
|
+
def self.serialize: (untyped resource) -> untyped
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Uniform { data:, errors:, status: } envelope
|
|
146
|
+
module Wrapped
|
|
147
|
+
include ActionFigure::Formatter
|
|
148
|
+
|
|
149
|
+
def Ok: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
150
|
+
def Created: (resource: untyped, ?meta: untyped?) -> ActionFigure::response
|
|
151
|
+
def Accepted: (?resource: untyped?, ?meta: untyped?) -> ActionFigure::response
|
|
152
|
+
def UnprocessableContent: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
153
|
+
def NotFound: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
154
|
+
def Forbidden: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
155
|
+
def Conflict: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
156
|
+
def PaymentRequired: (errors: ActionFigure::error_hash) -> ActionFigure::response
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
module Testing
|
|
161
|
+
# Minitest assertions for action response results
|
|
162
|
+
module Minitest
|
|
163
|
+
def assert_Ok: (ActionFigure::response result, ?String? msg) -> void
|
|
164
|
+
def assert_Created: (ActionFigure::response result, ?String? msg) -> void
|
|
165
|
+
def assert_Accepted: (ActionFigure::response result, ?String? msg) -> void
|
|
166
|
+
def assert_NoContent: (ActionFigure::response result, ?String? msg) -> void
|
|
167
|
+
def assert_UnprocessableContent: (ActionFigure::response result, ?String? msg) -> void
|
|
168
|
+
def assert_NotFound: (ActionFigure::response result, ?String? msg) -> void
|
|
169
|
+
def assert_Forbidden: (ActionFigure::response result, ?String? msg) -> void
|
|
170
|
+
def assert_Conflict: (ActionFigure::response result, ?String? msg) -> void
|
|
171
|
+
def assert_PaymentRequired: (ActionFigure::response result, ?String? msg) -> void
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# RSpec custom matchers (be_Ok, be_Created, etc.)
|
|
175
|
+
module RSpec
|
|
176
|
+
MATCHERS: Hash[Symbol, Symbol]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
4
179
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: action_figure
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tad Thorley
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: concurrent-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: dry-validation
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -29,6 +43,7 @@ executables: []
|
|
|
29
43
|
extensions: []
|
|
30
44
|
extra_rdoc_files: []
|
|
31
45
|
files:
|
|
46
|
+
- CHANGELOG.md
|
|
32
47
|
- LICENSE.txt
|
|
33
48
|
- README.md
|
|
34
49
|
- Rakefile
|
|
@@ -38,6 +53,7 @@ files:
|
|
|
38
53
|
- docs/custom-formatters.md
|
|
39
54
|
- docs/integration-patterns.md
|
|
40
55
|
- docs/response-formatters.md
|
|
56
|
+
- docs/status-codes.md
|
|
41
57
|
- docs/testing.md
|
|
42
58
|
- docs/validation.md
|
|
43
59
|
- lib/action_figure.rb
|
|
@@ -60,6 +76,7 @@ licenses:
|
|
|
60
76
|
metadata:
|
|
61
77
|
homepage_uri: https://github.com/phaedryx/action_figure
|
|
62
78
|
source_code_uri: https://github.com/phaedryx/action_figure
|
|
79
|
+
changelog_uri: https://github.com/phaedryx/action_figure/blob/main/CHANGELOG.md
|
|
63
80
|
rubygems_mfa_required: 'true'
|
|
64
81
|
rdoc_options: []
|
|
65
82
|
require_paths:
|
|
@@ -75,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
75
92
|
- !ruby/object:Gem::Version
|
|
76
93
|
version: '0'
|
|
77
94
|
requirements: []
|
|
78
|
-
rubygems_version: 4.0.
|
|
95
|
+
rubygems_version: 4.0.3
|
|
79
96
|
specification_version: 4
|
|
80
97
|
summary: Fully-articulated controller actions
|
|
81
98
|
test_files: []
|