sober_swag 0.12.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,12 +17,6 @@ module SoberSwag
17
17
  include ::Dry::Types()
18
18
  end
19
19
 
20
- included do
21
- rescue_from Dry::Struct::Error do
22
- head :bad_request
23
- end
24
- end
25
-
26
20
  class_methods do
27
21
  ##
28
22
  # Define a new action with the given HTTP method, action name, and path.
@@ -96,7 +90,7 @@ module SoberSwag
96
90
  r = current_action_def
97
91
  raise UndefinedPathError unless r&.path_params_class
98
92
 
99
- r.path_params_class.new(request.path_parameters)
93
+ r.path_params_class.call(request.path_parameters)
100
94
  end
101
95
  end
102
96
 
@@ -110,7 +104,7 @@ module SoberSwag
110
104
  r = current_action_def
111
105
  raise UndefinedBodyError unless r&.request_body_class
112
106
 
113
- r.request_body_class.new(body_params)
107
+ r.request_body_class.call(body_params)
114
108
  end
115
109
  end
116
110
 
@@ -124,7 +118,7 @@ module SoberSwag
124
118
  r = current_action_def
125
119
  raise UndefinedQueryError unless r&.query_params_class
126
120
 
127
- r.query_params_class.new(request.query_parameters)
121
+ r.query_params_class.call(request.query_parameters)
128
122
  end
129
123
  end
130
124
 
@@ -9,28 +9,36 @@ module SoberSwag
9
9
  @action_name = action_name
10
10
  @response_serializers = {}
11
11
  @response_descriptions = {}
12
+ @tags = []
12
13
  end
13
14
 
14
15
  attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
15
16
 
16
17
  ##
17
- # What to parse the request body in to.
18
+ # What to parse the request body into.
18
19
  attr_reader :request_body_class
19
20
  ##
20
- # What to parse the request query_params in to
21
+ # What to parse the request query_params into.
21
22
  attr_reader :query_params_class
22
-
23
23
  ##
24
- # What to parse the path params into
24
+ # What to parse the path params into.
25
25
  attr_reader :path_params_class
26
26
 
27
+ ##
28
+ # Standard swagger tags.
29
+ def tags(*args)
30
+ return @tags if args.empty?
31
+
32
+ @tags = args.flatten
33
+ end
34
+
27
35
  ##
28
36
  # Define the request body, using SoberSwag's type-definition scheme.
29
37
  # The block passed will be used to define the body of a new sublcass of `base` (defaulted to {SoberSwag::InputObject}.)
30
38
  # If you want, you can also define utility methods in here
31
39
  def request_body(base = SoberSwag::InputObject, &block)
32
40
  @request_body_class = make_input_object!(base, &block)
33
- action_module.const_set('ResponseBody', @request_body_class)
41
+ action_module.const_set('RequestBody', @request_body_class)
34
42
  end
35
43
 
36
44
  ##
@@ -103,7 +111,7 @@ module SoberSwag
103
111
  def response(status_code, description, serializer = nil, &block)
104
112
  status_key = Rack::Utils.status_code(status_code)
105
113
 
106
- raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)
114
+ raise ArgumentError, 'Response defined!' if @response_serializers.key?(status_key)
107
115
 
108
116
  serializer ||= SoberSwag::OutputObject.define(&block)
109
117
  response_module.const_set(status_code.to_s.classify, serializer)
@@ -124,8 +132,22 @@ module SoberSwag
124
132
  end
125
133
 
126
134
  def make_input_object!(base, &block)
127
- Class.new(base, &block).tap do |e|
128
- e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
135
+ if base.is_a?(Class)
136
+ make_input_class(base, block)
137
+ elsif block
138
+ raise ArgumentError, 'passed a non-class base and a block to an input'
139
+ else
140
+ base
141
+ end
142
+ end
143
+
144
+ def make_input_class(base, block)
145
+ if block
146
+ Class.new(base, &block).tap do |e|
147
+ e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
148
+ end
149
+ else
150
+ base
129
151
  end
130
152
  end
131
153
  end
@@ -7,6 +7,7 @@ module SoberSwag
7
7
  # Please see the documentation for that class to see how it works.
8
8
  class InputObject < Dry::Struct
9
9
  transform_keys(&:to_sym)
10
+ include SoberSwag::Type::Named
10
11
 
11
12
  class << self
12
13
  ##
@@ -17,19 +18,51 @@ module SoberSwag
17
18
  @identifier || name.to_s.gsub('::', '.')
18
19
  end
19
20
 
21
+ def attribute(key, parent = SoberSwag::InputObject, &block)
22
+ raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
23
+
24
+ super(key, parent, &block)
25
+ end
26
+
27
+ def attribute?(key, parent = SoberSwag::InputObject, &block)
28
+ raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
29
+
30
+ super(key, parent, &block)
31
+ end
32
+
20
33
  def meta(*args)
34
+ original = self
35
+
21
36
  super(*args).tap do |result|
22
- result.identifier(identifier) if result.is_a?(Class) # pass on identifier
37
+ return result unless result.is_a?(Class)
38
+
39
+ result.define_singleton_method(:alias?) { true }
40
+ result.define_singleton_method(:alias_of) { original }
23
41
  end
24
42
  end
25
43
 
26
- def primitive(sym)
27
- SoberSwag::Types.const_get(sym)
44
+ ##
45
+ # .primitive is already defined on Dry::Struct, so forward to the superclass if
46
+ # not called as a way to get a primitive type
47
+ def primitive(*args)
48
+ if args.length == 1
49
+ SoberSwag::Types.const_get(args.first)
50
+ else
51
+ super
52
+ end
28
53
  end
29
54
 
30
55
  def param(sym)
31
56
  SoberSwag::Types::Params.const_get(sym)
32
57
  end
58
+
59
+ private
60
+
61
+ def valid_field_def?(parent, block)
62
+ return true if block.nil?
63
+
64
+ parent.is_a?(Class) && parent <= SoberSwag::InputObject
65
+ end
33
66
  end
34
67
  end
35
68
  end
@@ -2,29 +2,30 @@ module SoberSwag
2
2
  module Nodes
3
3
  ##
4
4
  # One attribute of an object.
5
- class Attribute
6
- def initialize(key, required, value)
5
+ class Attribute < Base
6
+ def initialize(key, required, value, meta = {})
7
7
  @key = key
8
8
  @required = required
9
9
  @value = value
10
+ @meta = meta
10
11
  end
11
12
 
12
13
  def deconstruct
13
- [key, required, value]
14
+ [key, required, value, meta]
14
15
  end
15
16
 
16
17
  def deconstruct_keys
17
- { key: key, required: required, value: value }
18
+ { key: key, required: required, value: value, meta: meta }
18
19
  end
19
20
 
20
- attr_reader :key, :required, :value
21
+ attr_reader :key, :required, :value, :meta
21
22
 
22
23
  def map(&block)
23
- self.class.new(key, required, value.map(&block))
24
+ self.class.new(key, required, value.map(&block), meta)
24
25
  end
25
26
 
26
27
  def cata(&block)
27
- block.call(self.class.new(key, required, value.cata(&block)))
28
+ block.call(self.class.new(key, required, value.cata(&block), meta))
28
29
  end
29
30
  end
30
31
  end
@@ -10,8 +10,8 @@ module SoberSwag
10
10
 
11
11
  attr_reader :values
12
12
 
13
- def map(&block)
14
- self.class.new(@values.map(&block))
13
+ def map
14
+ dup
15
15
  end
16
16
 
17
17
  def deconstruct
@@ -11,7 +11,7 @@ module SoberSwag
11
11
  attr_reader :value, :metadata
12
12
 
13
13
  def map(&block)
14
- self.class.new(block.call(value))
14
+ self.class.new(block.call(value), metadata.dup)
15
15
  end
16
16
 
17
17
  def deconstruct
@@ -17,8 +17,14 @@ module SoberSwag
17
17
  @fields << field
18
18
  end
19
19
 
20
- def view(name, &block)
21
- view = View.define(name, fields, &block)
20
+ def view(name, inherits: nil, &block)
21
+ initial_fields =
22
+ if inherits.nil? || inherits == :base
23
+ fields
24
+ else
25
+ find_view(inherits).fields
26
+ end
27
+ view = View.define(name, initial_fields, &block)
22
28
 
23
29
  view.identifier("#{@identifier}.#{name.to_s.classify}") if identifier
24
30
 
@@ -29,6 +35,12 @@ module SoberSwag
29
35
  @identifier = arg if arg
30
36
  @identifier
31
37
  end
38
+
39
+ private
40
+
41
+ def find_view(name)
42
+ @views.find { |view| view.name == name } || (raise ArgumentError, "no view #{name.inspect} defined!")
43
+ end
32
44
  end
33
45
  end
34
46
  end
@@ -7,11 +7,27 @@ module SoberSwag
7
7
  add_field!(Field.new(name, serializer, from: from, &block))
8
8
  end
9
9
 
10
+ ##
11
+ # Similar to #field, but adds multiple at once.
12
+ # Named #multi because #fields was already taken.
13
+ def multi(names, serializer)
14
+ names.each { |name| field(name, serializer) }
15
+ end
16
+
10
17
  ##
11
18
  # Given a symbol to this, we will use a primitive name
12
19
  def primitive(name)
13
20
  SoberSwag::Serializer.primitive(SoberSwag::Types.const_get(name))
14
21
  end
22
+
23
+ ##
24
+ # Merge in anything that has a list of fields, and use it.
25
+ # Note that merging in a full blueprint *will not* also merge in views, just fields defined on the base.
26
+ def merge(other)
27
+ other.fields.each do |field|
28
+ add_field!(field)
29
+ end
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -25,7 +25,8 @@ module SoberSwag
25
25
  Nodes::Attribute.new(
26
26
  @node.name,
27
27
  @node.required? && !@node.type.default?,
28
- bind(Parser.new(@node.type))
28
+ bind(Parser.new(@node.type)),
29
+ @node.meta
29
30
  )
30
31
  when Dry::Types::Sum
31
32
  left = bind(Parser.new(@node.left))
@@ -46,13 +47,18 @@ module SoberSwag
46
47
  when Dry::Types::Constrained
47
48
  bind(Parser.new(@node.type))
48
49
  when Dry::Types::Nominal
49
- # start off with the moral equivalent of NodeTree[String]
50
- Nodes::Primitive.new(@node.primitive, @node.meta)
50
+ if @node.respond_to?(:type) && @node.type.is_a?(Dry::Types::Constrained)
51
+ bind(Parser.new(@node.type))
52
+ else
53
+ old_meta = @node.primitive.respond_to?(:meta) ? @node.primitive.meta : {}
54
+ # start off with the moral equivalent of NodeTree[String]
55
+ Nodes::Primitive.new(@node.primitive, old_meta.merge(@node.meta))
56
+ end
51
57
  else
52
58
  # Inside of this case we have a class that is some user-defined type
53
59
  # We put it in our array of found types, and consider it a primitive
54
60
  @found.add(@node)
55
- Nodes::Primitive.new(@node)
61
+ Nodes::Primitive.new(@node, @node.respond_to?(:meta) ? @node.meta : {})
56
62
  end
57
63
  end
58
64
 
@@ -17,6 +17,8 @@ module SoberSwag
17
17
  SoberSwag::Serializer::Optional.new(self)
18
18
  end
19
19
 
20
+ alias nilable optional
21
+
20
22
  ##
21
23
  # Is this type lazily defined?
22
24
  #
@@ -33,8 +33,11 @@ module SoberSwag
33
33
  # Using .meta on dry-struct returns a *new type* that wraps the old one.
34
34
  # As such, we need to be a bit clever about when we tack on the identifier
35
35
  # for this type.
36
- lazy_type.identifier(@base.lazy_type.identifier)
37
- type.identifier(@base.type.identifier)
36
+ %i[lazy_type type].each do |sym|
37
+ if @base.public_send(sym).respond_to?(:identifier) && public_send(sym).respond_to?(:identifier)
38
+ public_send(sym).identifier(@base.public_send(sym).identifier)
39
+ end
40
+ end
38
41
  end
39
42
 
40
43
  def lazy_type?
@@ -0,0 +1,7 @@
1
+ module SoberSwag
2
+ ##
3
+ # Namespace for type-definition-related utilities
4
+ module Type
5
+ autoload(:Named, 'sober_swag/type/named')
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ module SoberSwag
2
+ module Type
3
+ ##
4
+ # Mixin module used to identify types that should be considered
5
+ # standalone, named types from SoberSwag's perspective.
6
+ module Named
7
+ ##
8
+ # Class Methods Module.
9
+ # Modules that include {SoberSwag::Type::Named}
10
+ # will automatically extend this module.
11
+ module ClassMethods
12
+ def alias?
13
+ false
14
+ end
15
+
16
+ def alias_of
17
+ nil
18
+ end
19
+
20
+ def root_alias
21
+ alias_of || self
22
+ end
23
+
24
+ def description(arg = nil)
25
+ @description = arg if arg
26
+ @description
27
+ end
28
+ end
29
+
30
+ def self.included(mod)
31
+ mod.extend(ClassMethods)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -4,5 +4,7 @@ module SoberSwag
4
4
  # You can use constants like SoberSwag::Types::Integer and things as a result of this module existing.
5
5
  class Types
6
6
  include ::Dry::Types()
7
+
8
+ autoload(:CommaArray, 'sober_swag/types/comma_array')
7
9
  end
8
10
  end
@@ -0,0 +1,17 @@
1
+ module SoberSwag
2
+ class Types
3
+ ##
4
+ # An array that will be parsed from comma-separated values in a string, if given a string.
5
+ module CommaArray
6
+ def self.of(other)
7
+ SoberSwag::Types::Array.of(other).constructor { |val|
8
+ if val.is_a?(::String)
9
+ val.split(',').map(&:strip)
10
+ else
11
+ val
12
+ end
13
+ }.meta(style: :form, explode: false)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.12.0'
4
+ VERSION = '0.17.0'
5
5
  end
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency 'pry-byebug'
40
40
  spec.add_development_dependency 'rake', '~> 13.0'
41
41
  spec.add_development_dependency 'rspec', '~> 3.0'
42
- spec.add_development_dependency 'rubocop'
43
- spec.add_development_dependency 'rubocop-rspec'
42
+ spec.add_development_dependency 'rubocop', '~> 0.93.1'
43
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.44.1'
44
44
  spec.add_development_dependency 'simplecov'
45
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sober_swag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Super
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-24 00:00:00.000000000 Z
11
+ date: 2020-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -126,30 +126,30 @@ dependencies:
126
126
  name: rubocop
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: 0.93.1
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: 0.93.1
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rubocop-rspec
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ">="
143
+ - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0'
145
+ version: 1.44.1
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ">="
150
+ - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0'
152
+ version: 1.44.1
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: simplecov
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -179,11 +179,13 @@ files:
179
179
  - ".rubocop.yml"
180
180
  - ".ruby-version"
181
181
  - ".travis.yml"
182
+ - CHANGELOG.md
182
183
  - Gemfile
183
184
  - LICENSE.txt
184
185
  - README.md
185
186
  - Rakefile
186
187
  - bin/console
188
+ - bin/rspec
187
189
  - bin/setup
188
190
  - docs/serializers.md
189
191
  - example/.gitignore
@@ -249,6 +251,7 @@ files:
249
251
  - lib/sober_swag/compiler/error.rb
250
252
  - lib/sober_swag/compiler/path.rb
251
253
  - lib/sober_swag/compiler/paths.rb
254
+ - lib/sober_swag/compiler/primitive.rb
252
255
  - lib/sober_swag/compiler/type.rb
253
256
  - lib/sober_swag/controller.rb
254
257
  - lib/sober_swag/controller/route.rb
@@ -284,7 +287,10 @@ files:
284
287
  - lib/sober_swag/serializer/optional.rb
285
288
  - lib/sober_swag/serializer/primitive.rb
286
289
  - lib/sober_swag/server.rb
290
+ - lib/sober_swag/type.rb
291
+ - lib/sober_swag/type/named.rb
287
292
  - lib/sober_swag/types.rb
293
+ - lib/sober_swag/types/comma_array.rb
288
294
  - lib/sober_swag/version.rb
289
295
  - sober_swag.gemspec
290
296
  homepage: https://github.com/SonderMindOrg/sober_swag