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 +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
|