sober_swag 0.11.0 → 0.16.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
 
@@ -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