sober_swag 0.11.0 → 0.16.0

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.
@@ -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
 
@@ -14,14 +14,13 @@ module SoberSwag
14
14
  attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
15
15
 
16
16
  ##
17
- # What to parse the request body in to.
17
+ # What to parse the request body into.
18
18
  attr_reader :request_body_class
19
19
  ##
20
- # What to parse the request query_params in to
20
+ # What to parse the request query_params into.
21
21
  attr_reader :query_params_class
22
-
23
22
  ##
24
- # What to parse the path params into
23
+ # What to parse the path params into.
25
24
  attr_reader :path_params_class
26
25
 
27
26
  ##
@@ -30,7 +29,7 @@ module SoberSwag
30
29
  # If you want, you can also define utility methods in here
31
30
  def request_body(base = SoberSwag::InputObject, &block)
32
31
  @request_body_class = make_input_object!(base, &block)
33
- action_module.const_set('ResponseBody', @request_body_class)
32
+ action_module.const_set('RequestBody', @request_body_class)
34
33
  end
35
34
 
36
35
  ##
@@ -103,7 +102,7 @@ module SoberSwag
103
102
  def response(status_code, description, serializer = nil, &block)
104
103
  status_key = Rack::Utils.status_code(status_code)
105
104
 
106
- raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)
105
+ raise ArgumentError, 'Response defined!' if @response_serializers.key?(status_key)
107
106
 
108
107
  serializer ||= SoberSwag::OutputObject.define(&block)
109
108
  response_module.const_set(status_code.to_s.classify, serializer)
@@ -124,8 +123,22 @@ module SoberSwag
124
123
  end
125
124
 
126
125
  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)
126
+ if base.is_a?(Class)
127
+ make_input_class(base, block)
128
+ elsif block
129
+ raise ArgumentError, 'passed a non-class base and a block to an input'
130
+ else
131
+ base
132
+ end
133
+ end
134
+
135
+ def make_input_class(base, block)
136
+ if block
137
+ Class.new(base, &block).tap do |e|
138
+ e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
139
+ end
140
+ else
141
+ base
129
142
  end
130
143
  end
131
144
  end
@@ -7,22 +7,62 @@ 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
  ##
13
14
  # The name to use for this type in external documentation.
14
15
  def identifier(arg = nil)
15
16
  @identifier = arg if arg
17
+
16
18
  @identifier || name.to_s.gsub('::', '.')
17
19
  end
18
20
 
19
- def primitive(sym)
20
- SoberSwag::Types.const_get(sym)
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
+
33
+ def meta(*args)
34
+ original = self
35
+
36
+ super(*args).tap do |result|
37
+ return result unless result.is_a?(Class)
38
+
39
+ result.define_singleton_method(:alias?) { true }
40
+ result.define_singleton_method(:alias_of) { original }
41
+ end
42
+ end
43
+
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
21
53
  end
22
54
 
23
55
  def param(sym)
24
56
  SoberSwag::Types::Params.const_get(sym)
25
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
26
66
  end
27
67
  end
28
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
  #
@@ -20,20 +20,24 @@ module SoberSwag
20
20
  self.class.new(base, metadata.merge(hash))
21
21
  end
22
22
 
23
- ##
24
- # Delegates to `base`, adds metadata, pumbs identifiers
25
23
  def lazy_type
26
- @base.lazy_type.meta(**metadata).tap { |t| t.identifier(@base.identifier) }
24
+ @lazy_type ||= @base.lazy_type.meta(**metadata)
27
25
  end
28
26
 
29
- ##
30
- # Delegates to `base`, adds metadata, plumbs identifiers
31
27
  def type
32
- @base.type.meta(**metadata).tap { |t| t.identifier(@base.identifier) }
28
+ @type ||= @base.type.meta(**metadata)
33
29
  end
34
30
 
35
31
  def finalize_lazy_type!
36
32
  @base.finalize_lazy_type!
33
+ # Using .meta on dry-struct returns a *new type* that wraps the old one.
34
+ # As such, we need to be a bit clever about when we tack on the identifier
35
+ # for this type.
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
37
41
  end
38
42
 
39
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.11.0'
4
+ VERSION = '0.16.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