sober_swag 0.10.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: 528d6bf46f7f7821eb2009cafaa55fd5536a29dbfffd8a3ada81319089e08f89
4
- data.tar.gz: b484dd89af8defeb7c878403f843793f03b2695af543a07843ca72417f9cc4ec
3
+ metadata.gz: ff24aa407a6b8360569931c050bee2958cb6e3169f34f57f1d455f0d573327af
4
+ data.tar.gz: 61bf1feb701ae67e2a2fd85f41ab2d8ccd25d5e512a4cd26e3849f65e78a14e9
5
5
  SHA512:
6
- metadata.gz: db8538a1e6416be18ce5b08517a1cf90c48192936b820a689af79870b6fc4b12e392a97a4fdbb01086952aa443212fc1d5bb87c64f389a86ee2696c2fd8a8868
7
- data.tar.gz: cf386e6e54f6dafb5bd307196b5d193577d04ce56e986c03e1b69bb14592963fd6c226a7220b0074c7ea446582d3b361f05250033203448eeec84313da2b26b5
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.
@@ -1,48 +1,36 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
2
 
4
3
  require 'bundler/setup'
5
4
  require 'sober_swag'
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
- module Types
10
- include Dry.Types()
11
- end
5
+ require 'pry'
12
6
 
13
7
  Bio = SoberSwag.input_object do
14
- attribute :name, primitive(:String).meta(description: 'A very basic bio name')
8
+ attribute :description, SoberSwag::Types::String
9
+ attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
15
10
  end
16
11
 
17
12
  Person = SoberSwag.input_object do
18
- attribute :name, primitive(:String).meta(description: 'The full name description')
19
- attribute :age, param(:Integer).constrained(gt: 0).optional.meta(description: 'My cool age')
20
- attribute? :mood, Types::String
13
+ attribute :name, SoberSwag::Types::String
14
+ attribute? :bio, Bio.optional
21
15
  end
22
16
 
23
- ##
24
- # Demonstration of subclass-style.
25
- class PersonSearch < SoberSwag::InputObject
26
- attribute? :name, Types::String
27
- attribute? :age, Types::Integer
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
28
21
  end
29
22
 
30
- ##
31
- # Demonstration of subclass-style *and* recursion in structs.
32
- class LinkedList < SoberSwag::InputObject
33
- attribute :value, Types::String
34
- attribute? :next, LinkedList
23
+ SingleFloorLocation = SoberSwag.input_object do
24
+ attribute :building, SoberSwag::Types::String.enum('philosophy', 'computer science')
25
+ attribute :room, SoberSwag::Types::Integer
35
26
  end
36
27
 
37
- Foo = SoberSwag::OutputObject.define do
38
- field :name, primitive(:String)
39
- field :age, primitive(:String)
40
-
41
- view :foo do
42
- field :bar, primitive(:String).optional
43
- end
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')
44
32
  end
45
33
 
46
- # (If you use this, don't forget to add pry to your Gemfile!)
47
- require 'pry'
34
+ SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
35
+
48
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,20 +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
- { :$ref => self.class.get_ref(type) }
130
- when Dry::Types::Constrained
131
- self.class.new(type.type).schema_stub
132
- when Dry::Types::Array::Member
133
- { type: :array, items: self.class.new(type.member).schema_stub }
134
- when Dry::Types::Sum
135
- { 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
136
101
  else
137
- raise SoberSwag::Compiler::Error, "Cannot generate a schema stub for #{type} (#{type.class})"
102
+ object_schema
138
103
  end
139
104
  end
140
105
 
@@ -147,6 +112,10 @@ module SoberSwag
147
112
  end
148
113
  end
149
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
+
150
119
  def normalize(object)
151
120
  object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
152
121
  end
@@ -178,29 +147,14 @@ module SoberSwag
178
147
  end
179
148
  end
180
149
 
181
- 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
182
151
  case object
183
152
  when Nodes::List
184
- {
185
- type: :array,
186
- items: object.deconstruct.first
187
- }
153
+ { type: :array, items: object.element }
188
154
  when Nodes::Enum
189
- {
190
- type: :string,
191
- enum: object.deconstruct.first
192
- }
155
+ { type: :string, enum: object.values }
193
156
  when Nodes::OneOf
194
- if object.deconstruct.include?({ type: 'null' })
195
- rejected = object.deconstruct.reject { |e| e[:type] == 'null' }
196
- if rejected.length == 1
197
- rejected.first.merge(nullable: true)
198
- else
199
- { oneOf: rejected, nullable: true }
200
- end
201
- else
202
- { oneOf: object.deconstruct }
203
- end
157
+ one_of_to_schema(object)
204
158
  when Nodes::Object
205
159
  # openAPI requires that you give a list of required attributes
206
160
  # (which IMO is the *totally* wrong thing to do but whatever)
@@ -214,40 +168,50 @@ module SoberSwag
214
168
  required: required
215
169
  }
216
170
  when Nodes::Attribute
217
- name, req, value = object.deconstruct
171
+ name, req, value, meta = object.deconstruct
172
+ value = value.merge(meta&.select { |k, _| metadata_keys.include?(k) } || {})
218
173
  if req
219
174
  [name, value.merge(required: true)]
220
175
  else
221
176
  [name, value]
222
177
  end
223
- # can't match on value directly as ruby uses `===` to match,
224
- # and classes use `===` to mean `is an instance of`, as
225
- # opposed to direct equality lmao
226
178
  when Nodes::Primitive
227
- value = object.value
228
- metadata = object.metadata
229
- if self.class.primitive?(value)
230
- md = self.class.primitive_def(value)
231
- METADATA_KEYS.select(&metadata.method(:key?)).reduce(md) do |definition, key|
232
- definition.merge(key => metadata[key])
233
- end
179
+ object.value.merge(object.metadata.select { |k, _| metadata_keys.include?(k) })
180
+ else
181
+ raise ArgumentError, "Got confusing node #{object} (#{object.class})"
182
+ end
183
+ end
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)
234
190
  else
235
- { '$ref': self.class.get_ref(value) }
191
+ { oneOf: flatten_oneofs_hash(rejected), nullable: true }
236
192
  end
237
193
  else
238
- raise ArgumentError, "Got confusing node #{object} (#{object.class})"
194
+ { oneOf: flatten_oneofs_hash(object.deconstruct) }
239
195
  end
240
196
  end
241
197
 
198
+ def flatten_oneofs_hash(object)
199
+ object.map { |h|
200
+ h[:oneOf] || h
201
+ }.flatten
202
+ end
203
+
242
204
  def path_schema_stub
243
205
  @path_schema_stub ||=
244
- object_schema[:properties].map do |k, v|
206
+ make_object_schema(metadata_keys: METADATA_KEYS | %i[style explode])[:properties].map do |k, v|
245
207
  # ensure_uncomplicated(k, v)
246
208
  {
247
209
  name: k,
248
- schema: v.reject { |key, _| %i[required nullable].include?(key) },
249
- required: object_schema[:required].include?(k) || false
250
- }
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? }
251
215
  end
252
216
  end
253
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,15 +7,28 @@ 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
 
21
+ def meta(*args)
22
+ original = self
23
+
24
+ super(*args).tap do |result|
25
+ return result unless result.is_a?(Class)
26
+
27
+ result.define_singleton_method(:alias?) { true }
28
+ result.define_singleton_method(:alias_of) { original }
29
+ end
30
+ end
31
+
19
32
  def primitive(sym)
20
33
  SoberSwag::Types.const_get(sym)
21
34
  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
  #
@@ -21,15 +21,23 @@ module SoberSwag
21
21
  end
22
22
 
23
23
  def lazy_type
24
- @base.lazy_type.meta(**metadata)
24
+ @lazy_type ||= @base.lazy_type.meta(**metadata)
25
25
  end
26
26
 
27
27
  def type
28
- @base.type.meta(**metadata)
28
+ @type ||= @base.type.meta(**metadata)
29
29
  end
30
30
 
31
31
  def finalize_lazy_type!
32
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
33
41
  end
34
42
 
35
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.10.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.10.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,6 +179,7 @@ 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
@@ -249,6 +250,7 @@ files:
249
250
  - lib/sober_swag/compiler/error.rb
250
251
  - lib/sober_swag/compiler/path.rb
251
252
  - lib/sober_swag/compiler/paths.rb
253
+ - lib/sober_swag/compiler/primitive.rb
252
254
  - lib/sober_swag/compiler/type.rb
253
255
  - lib/sober_swag/controller.rb
254
256
  - lib/sober_swag/controller/route.rb
@@ -284,7 +286,10 @@ files:
284
286
  - lib/sober_swag/serializer/optional.rb
285
287
  - lib/sober_swag/serializer/primitive.rb
286
288
  - lib/sober_swag/server.rb
289
+ - lib/sober_swag/type.rb
290
+ - lib/sober_swag/type/named.rb
287
291
  - lib/sober_swag/types.rb
292
+ - lib/sober_swag/types/comma_array.rb
288
293
  - lib/sober_swag/version.rb
289
294
  - sober_swag.gemspec
290
295
  homepage: https://github.com/SonderMindOrg/sober_swag