sober_swag 0.14.0 → 0.15.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.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +46 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +11 -0
- data/bin/console +36 -0
- data/docs/serializers.md +63 -0
- data/example/app/controllers/application_controller.rb +5 -0
- data/lib/sober_swag.rb +1 -0
- data/lib/sober_swag/compiler.rb +1 -0
- data/lib/sober_swag/compiler/primitive.rb +75 -0
- data/lib/sober_swag/compiler/type.rb +57 -96
- data/lib/sober_swag/controller.rb +0 -6
- data/lib/sober_swag/input_object.rb +7 -1
- data/lib/sober_swag/nodes/attribute.rb +8 -7
- data/lib/sober_swag/nodes/enum.rb +2 -2
- data/lib/sober_swag/nodes/primitive.rb +1 -1
- data/lib/sober_swag/output_object/definition.rb +14 -2
- data/lib/sober_swag/output_object/field_syntax.rb +16 -0
- data/lib/sober_swag/parser.rb +10 -5
- data/lib/sober_swag/serializer/base.rb +2 -0
- data/lib/sober_swag/serializer/meta.rb +3 -1
- data/lib/sober_swag/type.rb +7 -0
- data/lib/sober_swag/type/named.rb +35 -0
- data/lib/sober_swag/types.rb +2 -0
- data/lib/sober_swag/types/comma_array.rb +17 -0
- data/lib/sober_swag/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff24aa407a6b8360569931c050bee2958cb6e3169f34f57f1d455f0d573327af
|
4
|
+
data.tar.gz: 61bf1feb701ae67e2a2fd85f41ab2d8ccd25d5e512a4cd26e3849f65e78a14e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46c72426dcabb170bf8c5416e8583c254a9c627f8a6cf48424145dc688c9abcb2f7c6e61c87c55da0b8f66f9d076254cce3f815616c9f03eaa88aa14b19c72e4
|
7
|
+
data.tar.gz: a13ac7c92fdaec126d93a3400e13f93287c993bd19c82caa76cc662112e82983a64de5233adfce1cd30a32bfdca6ecc794b5f06a15f7cc2ffda0bbd155b1d0a5
|
data/.github/workflows/lint.yml
CHANGED
@@ -1,15 +1,52 @@
|
|
1
|
-
|
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
|
-
|
8
|
+
name: Ruby Lint
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
4
15
|
|
5
16
|
jobs:
|
6
|
-
|
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@
|
10
|
-
- name:
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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-
|
data/.rubocop.yml
CHANGED
@@ -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
|
data/CHANGELOG.md
ADDED
@@ -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.
|
data/bin/console
ADDED
@@ -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
|
data/docs/serializers.md
CHANGED
@@ -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`.
|
data/lib/sober_swag.rb
CHANGED
data/lib/sober_swag/compiler.rb
CHANGED
@@ -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
|
-
|
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|
|
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
|
-
|
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
|
125
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
|
@@ -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
|
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
|
@@ -17,8 +17,14 @@ module SoberSwag
|
|
17
17
|
@fields << field
|
18
18
|
end
|
19
19
|
|
20
|
-
def view(name, &block)
|
21
|
-
|
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
|
data/lib/sober_swag/parser.rb
CHANGED
@@ -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
|
-
|
50
|
-
|
51
|
-
|
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
|
|
@@ -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
|
-
|
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,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
|
data/lib/sober_swag/types.rb
CHANGED
@@ -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
|
data/lib/sober_swag/version.rb
CHANGED
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.
|
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-
|
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
|