sober_swag 0.14.0 → 0.15.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
  SHA256:
3
- metadata.gz: 234da6bca56fd440edae30e4fc9c34c50a37a6ab26b41739276e57ab43c73408
4
- data.tar.gz: 103e61429365ceb04d6d0225822ddaa4012f95a043a5bb97c6b8a2fd26671ce5
3
+ metadata.gz: ff24aa407a6b8360569931c050bee2958cb6e3169f34f57f1d455f0d573327af
4
+ data.tar.gz: 61bf1feb701ae67e2a2fd85f41ab2d8ccd25d5e512a4cd26e3849f65e78a14e9
5
5
  SHA512:
6
- metadata.gz: 58f27a7deebfcb50ec6279084e0a0e872fd0c2a447fca922549d7d3d9822a7e13e010372376c1a079472ba807845d267327275fe453efb99662f685321041e28
7
- data.tar.gz: 97ecdb6352db7545004fba2103bb505b3730973a22689f05dedd5b6a305115a63f302f0f248edd3f70e6377ebe470c9ef4f734b2e38af031bf762d229c71bcc0
6
+ metadata.gz: 46c72426dcabb170bf8c5416e8583c254a9c627f8a6cf48424145dc688c9abcb2f7c6e61c87c55da0b8f66f9d076254cce3f815616c9f03eaa88aa14b19c72e4
7
+ data.tar.gz: a13ac7c92fdaec126d93a3400e13f93287c993bd19c82caa76cc662112e82983a64de5233adfce1cd30a32bfdca6ecc794b5f06a15f7cc2ffda0bbd155b1d0a5
@@ -1,15 +1,52 @@
1
- name: Linters
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
2
7
 
3
- on: [push]
8
+ name: Ruby Lint
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
4
15
 
5
16
  jobs:
6
- build:
17
+ test:
18
+
7
19
  runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby: [ '2.6', '2.7' ]
23
+
8
24
  steps:
9
- - uses: actions/checkout@v1
10
- - name: Rubocop Linter
11
- uses: andrewmcodes/rubocop-linter-action@v3.0.0
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
+ # uses: ruby/setup-ruby@v1
30
+ uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
31
+ with:
32
+ ruby-version: ${{ matrix.ruby }}
33
+ - uses: actions/cache@v2
34
+ with:
35
+ path: vendor/bundle
36
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
37
+ restore-keys: |
38
+ ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
39
+ - name: Install dependencies
40
+ run: |
41
+ bundle config path vendor/bundle
42
+ bundle install
43
+ gem install rubocop
44
+ gem install rubocop-rspec
45
+ - name: Run Lints
46
+ run: rubocop lib spec example
47
+ - uses: actions/cache@v2
12
48
  with:
13
- action_config_path: '.github/config/rubocop_linter_action.yml' # Note: this is the default location
14
- env:
15
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+ path: example/vendor/bundle
50
+ key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
51
+ restore-keys: |
52
+ ${{ runner.os }}-${{ matrix.ruby }}-example-deps-
@@ -24,6 +24,8 @@ RSpec/ImplicitBlockExpectation:
24
24
  Enabled: false
25
25
  RSpec/ImplicitExpect:
26
26
  EnforcedStyle: should
27
+ RSpec/LeadingSubject:
28
+ Enabled: false
27
29
  Style/MultilineBlockChain:
28
30
  Enabled: false
29
31
  Metrics/AbcSize:
@@ -79,3 +81,5 @@ Style/RedundantRegexpEscape:
79
81
  Enabled: true
80
82
  Style/SlicingWithRange:
81
83
  Enabled: true
84
+ RSpec/NestedGroups:
85
+ Max: 5
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## V0.15.0: 2020-09-02
4
+
5
+ - Add `multi` to Output Objects, as a way to define more than one field of the same type at once.
6
+ - Add an `inherits:` key to output objects, for view inheritance.
7
+ - Add `SoberSwag::Types::CommaArray`, which parses comma-separated strings into arrays.
8
+ This also sets `style` to `form` and `explode` to `false` when generating Swagger docs.
9
+ This class is mostly useful for query parameters where you want a simpler format: `tag=foo,bar` instead of `tag[]=foo,tag[]=bar`.
10
+ - Add support for using `meta` to specify alternative `style` and `explode` keys for query and path params.
11
+ Note that this support *does not* extend to parsing: If you modify the `style` or `explode` keywords, you will need to make those input formats work with the actual type yourself.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sober_swag'
5
+ require 'pry'
6
+
7
+ Bio = SoberSwag.input_object do
8
+ attribute :description, SoberSwag::Types::String
9
+ attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
10
+ end
11
+
12
+ Person = SoberSwag.input_object do
13
+ attribute :name, SoberSwag::Types::String
14
+ attribute? :bio, Bio.optional
15
+ end
16
+
17
+ MultiFloorLocation = SoberSwag.input_object do
18
+ attribute :building, SoberSwag::Types::String.enum('science', 'mathematics', 'literature')
19
+ attribute :floor, SoberSwag::Types::String
20
+ attribute :room, SoberSwag::Types::Integer
21
+ end
22
+
23
+ SingleFloorLocation = SoberSwag.input_object do
24
+ attribute :building, SoberSwag::Types::String.enum('philosophy', 'computer science')
25
+ attribute :room, SoberSwag::Types::Integer
26
+ end
27
+
28
+ SchoolClass = SoberSwag.input_object do
29
+ attribute :prof, Person.meta(description: 'The person who teaches this class.')
30
+ attribute :students, SoberSwag::Types::Array.of(Person)
31
+ attribute :location, (SingleFloorLocation | MultiFloorLocation).meta(description: 'What building and room this is in')
32
+ end
33
+
34
+ SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
35
+
36
+ Pry.start
@@ -135,6 +135,22 @@ We can see a few things here:
135
135
  2. You must provide types with field names
136
136
  3. You can use blocks to do data formatting, which lets you pick different fields and such.
137
137
 
138
+
139
+ ### Multi
140
+
141
+ If you have a few fields of the same type, you can use `#multi` to define them all at once:
142
+
143
+ ```ruby
144
+ StudentOutputObject = SoberSwag::OutputObject.define do
145
+ multi [:first_name, :last_name], primitive(:String)
146
+ field :recent_grades, primitive(:Integer).array do |student|
147
+ student.graded_assignments.limit(100).pluck(:grade)
148
+ end
149
+ end
150
+ ```
151
+
152
+ This saves a bit of typing, and can help with refactoring later.
153
+
138
154
  ### Views
139
155
 
140
156
  Sometimes, you might want to add "variant" ways to look at data.
@@ -201,3 +217,50 @@ end
201
217
  ```
202
218
 
203
219
  For clarity (and to prevent infinitely-looping serializers on accident, we recommend you *always* use an explicit view for dependent output objects.
220
+
221
+ ### "Inheritance"
222
+
223
+ Output objects don't support inheritance.
224
+ You can't have one output object based on another.
225
+ You *can*, however, merge one into another!
226
+ Consdier this case:
227
+
228
+ ```ruby
229
+ GenericBioOutput = SoberSwag::OutputObject.define do
230
+ field :name, primitive(:String)
231
+ field :brief_history, primtiive(:String)
232
+ end
233
+
234
+ ExecutiveBioOutput = SoberSwag::OutputObject.define do
235
+ merge GenericBioOutput
236
+ field :company, primitive(:String)
237
+ field :position, primitive(:String)
238
+ end
239
+ ```
240
+
241
+ Using `#merge` lets you add in all the fields from one output object into another.
242
+ You can even use `merge` from within a view.
243
+
244
+ Note that `merge` does *not* copy anything but fields.
245
+ Identifiers and views will not be copied over.
246
+
247
+ ### View Inheritance
248
+
249
+ While defining a new Output Object, you *do not* have access to the definition of that output object.
250
+ So, how do I say that one view should be an extension of another?
251
+ Simple: use the `inherits:` kwarg:
252
+
253
+ ```ruby
254
+ BioOutput = SoberSwag::OutputObject.define do
255
+ field :name, primitive(:String)
256
+ view :detail do
257
+ field :bio, primitive(:String)
258
+ end
259
+ view :super_detail, inherits: :detail do
260
+ field :age, primitive(:Integer)
261
+ end
262
+ end
263
+ ```
264
+
265
+ `inherits` will automatically merge in all the fields of the referenced view.
266
+ This means that the view `super_detail` will include fields `name`, `bio`, and `age`.
@@ -1,2 +1,7 @@
1
+ ##
2
+ # Standard application controller.
1
3
  class ApplicationController < ActionController::API
4
+ rescue_from Dry::Struct::Error do
5
+ head :bad_request
6
+ end
2
7
  end
@@ -21,6 +21,7 @@ module SoberSwag
21
21
  autoload :Controller, 'sober_swag/controller'
22
22
  autoload :InputObject, 'sober_swag/input_object'
23
23
  autoload :Server, 'sober_swag/server'
24
+ autoload :Type, 'sober_swag/type'
24
25
 
25
26
  ##
26
27
  # Define a struct of something.
@@ -6,6 +6,7 @@ module SoberSwag
6
6
  class Compiler
7
7
  autoload(:Type, 'sober_swag/compiler/type')
8
8
  autoload(:Error, 'sober_swag/compiler/error')
9
+ autoload(:Primitive, 'sober_swag/compiler/primitive')
9
10
  autoload(:Path, 'sober_swag/compiler/path')
10
11
  autoload(:Paths, 'sober_swag/compiler/paths')
11
12
 
@@ -0,0 +1,75 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ ##
4
+ # Compiles a primitive type.
5
+ # Almost always constructed with the values from
6
+ # {SoberSwag::Nodes::Primitive}.
7
+ class Primitive
8
+ def initialize(type)
9
+ @type = type
10
+
11
+ raise Error, "#{type.inspect} is not a class!" unless @type.is_a?(Class)
12
+ end
13
+
14
+ attr_reader :type
15
+
16
+ def swagger_primitive?
17
+ SWAGGER_PRIMITIVE_DEFS.include?(type)
18
+ end
19
+
20
+ ##
21
+ # Is the wrapped type a named type, causing us to make a ref?
22
+ def named?
23
+ type <= SoberSwag::Type::Named
24
+ end
25
+
26
+ ##
27
+ # Turn this type into a swagger hash with a proper type key.
28
+ def type_hash
29
+ if swagger_primitive?
30
+ SWAGGER_PRIMITIVE_DEFS.fetch(type)
31
+ else
32
+ {
33
+ oneOf: [
34
+ { '$ref'.to_sym => named_ref }
35
+ ]
36
+ }
37
+ end
38
+ end
39
+
40
+ DATE_PRIMITIVE = { type: :string, format: :date }.freeze
41
+ DATE_TIME_PRIMITIVE = { type: :string, format: :'date-time' }.freeze
42
+
43
+ SWAGGER_PRIMITIVE_DEFS =
44
+ {
45
+ NilClass => :null,
46
+ TrueClass => :boolean,
47
+ FalseClass => :boolean,
48
+ Float => :number,
49
+ Integer => :integer,
50
+ String => :string
51
+ }.transform_values { |v| { type: v.freeze } }
52
+ .to_h.merge(
53
+ Date => DATE_PRIMITIVE,
54
+ DateTime => DATE_TIME_PRIMITIVE,
55
+ Time => DATE_TIME_PRIMITIVE
56
+ ).transform_values(&:freeze).freeze
57
+
58
+ def ref_name
59
+ raise Error, 'is not a type that is named!' if swagger_primitive?
60
+
61
+ if type <= SoberSwag::Type::Named
62
+ type.root_alias.identifier
63
+ else
64
+ type.name.gsub('::', '.')
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def named_ref
71
+ "#/components/schemas/#{ref_name}"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -4,46 +4,6 @@ module SoberSwag
4
4
  # A compiler for DRY-Struct data types, essentially.
5
5
  # It only consumes one type at a time.
6
6
  class Type # rubocop:disable Metrics/ClassLength
7
- class << self
8
- def get_ref(klass)
9
- "#/components/schemas/#{safe_name(klass)}"
10
- end
11
-
12
- def safe_name(klass)
13
- if klass.respond_to?(:identifier)
14
- klass.identifier
15
- else
16
- klass.to_s.gsub('::', '.')
17
- end
18
- end
19
-
20
- def primitive?(value)
21
- primitive_def(value) != nil
22
- end
23
-
24
- def primitive_def(value)
25
- value = value.primitive if value.is_a?(Dry::Types::Nominal)
26
-
27
- return nil unless value.is_a?(Class)
28
-
29
- if (name = primitive_name(value))
30
- { type: name }
31
- elsif value == Date
32
- { type: 'string', format: 'date' }
33
- elsif [Time, DateTime].any?(&value.ancestors.method(:include?))
34
- { type: 'string', format: 'date-time' }
35
- end
36
- end
37
-
38
- def primitive_name(value)
39
- return 'null' if value == NilClass
40
- return 'integer' if value == Integer
41
- return 'number' if value == Float
42
- return 'string' if value == String
43
- return 'boolean' if [TrueClass, FalseClass].include?(value)
44
- end
45
- end
46
-
47
7
  class TooComplicatedError < ::SoberSwag::Compiler::Error; end
48
8
  class TooComplicatedForPathError < TooComplicatedError; end
49
9
  class TooComplicatedForQueryError < TooComplicatedError; end
@@ -65,7 +25,15 @@ module SoberSwag
65
25
 
66
26
  def object_schema
67
27
  @object_schema ||=
68
- normalize(parsed_type).cata(&method(:to_object_schema))
28
+ make_object_schema
29
+ end
30
+
31
+ def object_schema_meta
32
+ return {} unless standalone? && type <= SoberSwag::Type::Named
33
+
34
+ {
35
+ description: type.description
36
+ }.reject { |_, v| v.nil? }
69
37
  end
70
38
 
71
39
  def schema_stub
@@ -81,14 +49,16 @@ module SoberSwag
81
49
  raise TooComplicatedForPathError, e.message
82
50
  end
83
51
 
52
+ DEFAULT_QUERY_SCHEMA_ATTRS = { in: :query, style: :deepObject, explode: true }.freeze
53
+
84
54
  def query_schema
85
- path_schema_stub.map { |e| e.merge(in: :query, style: :deepObject, explode: true) }
55
+ path_schema_stub.map { |e| DEFAULT_QUERY_SCHEMA_ATTRS.merge(e) }
86
56
  rescue TooComplicatedError => e
87
57
  raise TooComplicatedForQueryError, e.message
88
58
  end
89
59
 
90
60
  def ref_name
91
- self.class.safe_name(type)
61
+ SoberSwag::Compiler::Primitive.new(type).ref_name
92
62
  end
93
63
 
94
64
  def found_types
@@ -99,6 +69,10 @@ module SoberSwag
99
69
  end
100
70
  end
101
71
 
72
+ def mapped_type
73
+ @mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
74
+ end
75
+
102
76
  def parsed_type
103
77
  @parsed_type ||=
104
78
  begin
@@ -121,22 +95,11 @@ module SoberSwag
121
95
 
122
96
  private
123
97
 
124
- def generate_schema_stub # rubocop:disable Metrics/MethodLength
125
- return self.class.primitive_def(type) if self.class.primitive?(type)
126
-
127
- case type
128
- when Class
129
- # refs have to be standalone
130
- # so to not interefere with our other stuff, do this horrible garbage
131
- { oneOf: [{ '$ref'.to_sym => self.class.get_ref(type) }] }
132
- when Dry::Types::Constrained
133
- self.class.new(type.type).schema_stub
134
- when Dry::Types::Array::Member
135
- { type: :array, items: self.class.new(type.member).schema_stub }
136
- when Dry::Types::Sum
137
- { oneOf: normalize(parsed_type).elements.map { |t| self.class.new(t.value).schema_stub } }
98
+ def generate_schema_stub
99
+ if type.is_a?(Class)
100
+ SoberSwag::Compiler::Primitive.new(type).type_hash
138
101
  else
139
- raise SoberSwag::Compiler::Error, "Cannot generate a schema stub for #{type} (#{type.class})"
102
+ object_schema
140
103
  end
141
104
  end
142
105
 
@@ -149,6 +112,10 @@ module SoberSwag
149
112
  end
150
113
  end
151
114
 
115
+ def make_object_schema(metadata_keys: METADATA_KEYS)
116
+ normalize(mapped_type).cata { |e| to_object_schema(e, metadata_keys) }.merge(object_schema_meta)
117
+ end
118
+
152
119
  def normalize(object)
153
120
  object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
154
121
  end
@@ -180,29 +147,14 @@ module SoberSwag
180
147
  end
181
148
  end
182
149
 
183
- def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
150
+ def to_object_schema(object, metadata_keys = METADATA_KEYS) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
184
151
  case object
185
152
  when Nodes::List
186
- {
187
- type: :array,
188
- items: object.deconstruct.first
189
- }
153
+ { type: :array, items: object.element }
190
154
  when Nodes::Enum
191
- {
192
- type: :string,
193
- enum: object.deconstruct.first
194
- }
155
+ { type: :string, enum: object.values }
195
156
  when Nodes::OneOf
196
- if object.deconstruct.include?({ type: 'null' })
197
- rejected = object.deconstruct.reject { |e| e[:type] == 'null' }
198
- if rejected.length == 1
199
- rejected.first.merge(nullable: true)
200
- else
201
- { oneOf: rejected, nullable: true }
202
- end
203
- else
204
- { oneOf: object.deconstruct }
205
- end
157
+ one_of_to_schema(object)
206
158
  when Nodes::Object
207
159
  # openAPI requires that you give a list of required attributes
208
160
  # (which IMO is the *totally* wrong thing to do but whatever)
@@ -216,41 +168,50 @@ module SoberSwag
216
168
  required: required
217
169
  }
218
170
  when Nodes::Attribute
219
- name, req, value = object.deconstruct
171
+ name, req, value, meta = object.deconstruct
172
+ value = value.merge(meta&.select { |k, _| metadata_keys.include?(k) } || {})
220
173
  if req
221
174
  [name, value.merge(required: true)]
222
175
  else
223
176
  [name, value]
224
177
  end
225
178
  when Nodes::Primitive
226
- value = object.value
227
- metadata = object.metadata
228
- type_def =
229
- if self.class.primitive?(value)
230
- self.class.primitive_def(value)
231
- else
232
- metadata.merge!(value.meta)
233
- # refs have to be on their own, this is the stupid workaround
234
- # so you can add descriptions and stuff
235
- { oneOf: [{ '$ref'.to_sym => self.class.get_ref(value) }] }
236
- end
237
- METADATA_KEYS.select(&metadata.method(:key?)).reduce(type_def) do |definition, key|
238
- definition.merge(key => metadata[key])
239
- end
179
+ object.value.merge(object.metadata.select { |k, _| metadata_keys.include?(k) })
240
180
  else
241
181
  raise ArgumentError, "Got confusing node #{object} (#{object.class})"
242
182
  end
243
183
  end
244
184
 
185
+ def one_of_to_schema(object)
186
+ if object.deconstruct.include?({ type: :null })
187
+ rejected = object.deconstruct.reject { |e| e[:type] == :null }
188
+ if rejected.length == 1
189
+ rejected.first.merge(nullable: true)
190
+ else
191
+ { oneOf: flatten_oneofs_hash(rejected), nullable: true }
192
+ end
193
+ else
194
+ { oneOf: flatten_oneofs_hash(object.deconstruct) }
195
+ end
196
+ end
197
+
198
+ def flatten_oneofs_hash(object)
199
+ object.map { |h|
200
+ h[:oneOf] || h
201
+ }.flatten
202
+ end
203
+
245
204
  def path_schema_stub
246
205
  @path_schema_stub ||=
247
- object_schema[:properties].map do |k, v|
206
+ make_object_schema(metadata_keys: METADATA_KEYS | %i[style explode])[:properties].map do |k, v|
248
207
  # ensure_uncomplicated(k, v)
249
208
  {
250
209
  name: k,
251
- schema: v.reject { |key, _| %i[required nullable].include?(key) },
252
- required: object_schema[:required].include?(k) || false
253
- }
210
+ schema: v.reject { |key, _| %i[required nullable explode style].include?(key) },
211
+ required: object_schema[:required].include?(k) || false,
212
+ style: v[:style],
213
+ explode: v[:explode]
214
+ }.reject { |_, v2| v2.nil? }
254
215
  end
255
216
  end
256
217
 
@@ -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.
@@ -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
  ##
@@ -18,8 +19,13 @@ module SoberSwag
18
19
  end
19
20
 
20
21
  def meta(*args)
22
+ original = self
23
+
21
24
  super(*args).tap do |result|
22
- result.identifier(identifier) if result.is_a?(Class) # pass on identifier
25
+ return result unless result.is_a?(Class)
26
+
27
+ result.define_singleton_method(:alias?) { true }
28
+ result.define_singleton_method(:alias_of) { original }
23
29
  end
24
30
  end
25
31
 
@@ -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,14 +47,18 @@ module SoberSwag
46
47
  when Dry::Types::Constrained
47
48
  bind(Parser.new(@node.type))
48
49
  when Dry::Types::Nominal
49
- old_meta = @node.primitive.respond_to?(:meta) ? @node.primitive.meta : {}
50
- # start off with the moral equivalent of NodeTree[String]
51
- Nodes::Primitive.new(@node.primitive, old_meta.merge(@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
52
57
  else
53
58
  # Inside of this case we have a class that is some user-defined type
54
59
  # We put it in our array of found types, and consider it a primitive
55
60
  @found.add(@node)
56
- Nodes::Primitive.new(@node)
61
+ Nodes::Primitive.new(@node, @node.respond_to?(:meta) ? @node.meta : {})
57
62
  end
58
63
  end
59
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
  #
@@ -34,7 +34,9 @@ module SoberSwag
34
34
  # As such, we need to be a bit clever about when we tack on the identifier
35
35
  # for this type.
36
36
  %i[lazy_type type].each do |sym|
37
- public_send(sym).identifier(@base.public_send(sym).identifier) if @base.public_send(sym).respond_to?(:identifier)
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
38
40
  end
39
41
  end
40
42
 
@@ -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.14.0'
4
+ VERSION = '0.15.0'
5
5
  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.14.0
4
+ version: 0.15.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-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -179,10 +179,12 @@ 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
187
+ - bin/console
186
188
  - bin/setup
187
189
  - docs/serializers.md
188
190
  - example/.gitignore
@@ -248,6 +250,7 @@ files:
248
250
  - lib/sober_swag/compiler/error.rb
249
251
  - lib/sober_swag/compiler/path.rb
250
252
  - lib/sober_swag/compiler/paths.rb
253
+ - lib/sober_swag/compiler/primitive.rb
251
254
  - lib/sober_swag/compiler/type.rb
252
255
  - lib/sober_swag/controller.rb
253
256
  - lib/sober_swag/controller/route.rb
@@ -283,7 +286,10 @@ files:
283
286
  - lib/sober_swag/serializer/optional.rb
284
287
  - lib/sober_swag/serializer/primitive.rb
285
288
  - lib/sober_swag/server.rb
289
+ - lib/sober_swag/type.rb
290
+ - lib/sober_swag/type/named.rb
286
291
  - lib/sober_swag/types.rb
292
+ - lib/sober_swag/types/comma_array.rb
287
293
  - lib/sober_swag/version.rb
288
294
  - sober_swag.gemspec
289
295
  homepage: https://github.com/SonderMindOrg/sober_swag