sober_swag 0.5.0 → 0.6.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: f00dc3f047ad2bc40f12096ff56ebfd6ba3018cc05d0115ce400bc851cc382f9
4
- data.tar.gz: 0f9819bf9bc3135b86fd2dde790e58477b0e3c0865a580987a6203a32a8eec34
3
+ metadata.gz: cdf3896754f6acf3bef2df51e947439be3822d3635eb924d6029ab3157b13bd9
4
+ data.tar.gz: 75846c0b78e4e8dc9fc550af50aeb2bb73f3c1200e8c50af6c07092d2dd8c277
5
5
  SHA512:
6
- metadata.gz: e41afbd4771aca33e626c5bbad84cc5826ccf11c042878f9453a3cffb2d396b3f58e1d254c3fdf8cd5a847c079a697f68972e9e129f589af1e3b084208954775
7
- data.tar.gz: 17b7771051bbde79bedcd1c4094f32cb612e24a5d9dfbe05b522bf086e55dbf43dae6ebf05d0c9cc447b3e8a8600918a83885fac1526ec598938b97f4ca5b1de
6
+ metadata.gz: ae7076a9dcabcf2a99b642f967782195d1680ff13b6fd251123abdec15ec2eab18435ec1897fe212a060779b1bdffb8061bcb4b2eb89884713230ca0c70ecc27
7
+ data.tar.gz: 5f3fb9ee3c31feb8ede072171e5f0f2f5108061d7f09c44e4f4d88d929c001519734fce64b4c261ea4d8e63d9aa1862fdf374cdddd00da2b58d9cf75fc367320
@@ -2,4 +2,3 @@ check_name: 'Rubocop Lint'
2
2
  versions:
3
3
  rubocop: 'latest'
4
4
  rubocop-rspec: 'latest'
5
-
data/.gitignore CHANGED
@@ -10,5 +10,6 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  .ruby-version
13
+ Gemfile.lock
13
14
 
14
15
  *.gem
@@ -3,7 +3,7 @@ Style/FrozenStringLiteralComment:
3
3
  Style/BlockDelimiters:
4
4
  EnforcedStyle: braces_for_chaining
5
5
  AllCops:
6
- TargetRubyVersion: 2.7.1
6
+ TargetRubyVersion: 2.6.0
7
7
  Exclude:
8
8
  - 'bin/bundle'
9
9
  - 'example/bin/bundle'
@@ -31,6 +31,8 @@ Metrics/AbcSize:
31
31
  Style/Documentation:
32
32
  Exclude:
33
33
  - 'example/db/migrate/**/*'
34
+ Metrics/PerceivedComplexity:
35
+ Enabled: false
34
36
  Layout/EmptyLinesAroundAttributeAccessor:
35
37
  Enabled: true
36
38
  Layout/SpaceAroundMethodCallOperator:
data/README.md CHANGED
@@ -4,13 +4,11 @@
4
4
  ![Linters Status](https://github.com/SonderMindOrg/sober_swag/workflows/Linters/badge.svg?branch=master)
5
5
 
6
6
  SoberSwag is a combination of [Dry-Types](https://dry-rb.org/gems/dry-types/1.2/) and [Swagger](https://swagger.io/) that makes your Rails APIs more awesome.
7
- Other tools generate documenation from a DSL.
7
+ Other tools generate documentation from a DSL.
8
8
  This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids.
9
9
 
10
10
  An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
11
11
 
12
- This gem uses pattern matching, and is thus only compatible with Ruby 2.7 or later.
13
-
14
12
  ## Types for a fully-automated API
15
13
 
16
14
  SoberSwag lets you type your API using describe blocks.
@@ -18,22 +16,22 @@ In any controller that includes `SoberSwag::Controller`, you get access to the s
18
16
  This lets you type your API endpoint:
19
17
 
20
18
  ```ruby
21
- class PeopleController < ApplicationController
22
- include SoberSwag::Controller
23
- define :patch, :update, '/people/{id}' do
24
- query_params do
25
- attribute? :include_extra_info, Types::Params::Bool
26
- end
27
- request_body do
28
- attribute? :name, Types::Params::String
29
- attribute? :age, Types::Params::Integer
30
- end
31
- path_params { attribute :id, Types::Params::Integer }
19
+ class PeopleController < ApplicationController
20
+ include SoberSwag::Controller
21
+ define :patch, :update, '/people/{id}' do
22
+ query_params do
23
+ attribute? :include_extra_info, Types::Params::Bool
24
+ end
25
+ request_body do
26
+ attribute? :name, Types::Params::String
27
+ attribute? :age, Types::Params::Integer
32
28
  end
29
+ path_params { attribute :id, Types::Params::Integer }
33
30
  end
31
+ end
34
32
  ```
35
33
 
36
- We can now us this information to generate swagger documentation, available at the `swagger` action on this controller.
34
+ We can now use this information to generate swagger documentation, available at the `swagger` action on this controller.
37
35
  More than that, we can use this information *inside* our controller methods:
38
36
 
39
37
  ```ruby
@@ -87,27 +85,75 @@ end
87
85
 
88
86
  Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development.
89
87
 
90
- ### SoberSwag Structs
88
+ ### SoberSwag Input Objects
91
89
 
92
90
  Input parameters (including path, query, and request body) are typed using [dry-struct](https://dry-rb.org/gems/dry-struct/1.0/).
93
- You don't have to do them inline: you can define them in another file, like so:
91
+ You don't have to do them inline. You can define them in another file, like so:
94
92
 
95
93
  ```ruby
96
- User = SoberSwag.struct do
94
+ User = SoberSwag.input_object do
97
95
  attribute :name, SoberSwag::Types::String
98
96
  # use ? if attributes are not required
99
97
  attribute? :favorite_movie, SoberSwag::Types::String
100
98
  # use .optional if attributes may be null
101
- attribute :age, SoberSwag::Types::Params::::Integer.optional
99
+ attribute :age, SoberSwag::Types::Params::Integer.optional
100
+ end
101
+ ```
102
+
103
+ Then, in your controller, just do:
104
+
105
+ ```ruby
106
+ class PeopleController < ApplicationController
107
+ include SoberSwag::Controller
108
+
109
+ define :path, :update, '/people/{id}' do
110
+ request_body(User)
111
+ path_params { attribute :id, Types::Params::Integer }
112
+ response(:ok, 'the updated person', PersonOutputObject)
113
+ end
114
+ def update
115
+ # same as above!
116
+ end
102
117
  end
103
118
  ```
104
119
 
105
120
  Under the hood, this literally just generates a subclass of `Dry::Struct`.
106
121
  We use the DSL-like method just to make working with Rails' reloading less annoying.
107
122
 
123
+ #### Adding additional documentation
124
+
125
+ You can use the `.meta` attribute on a type to add additional documentation.
126
+ Some keys are considered "well-known" and will be present on the swagger output.
127
+ For example:
128
+
129
+
130
+ ```ruby
131
+ User = SoberSwag.input_object do
132
+ attribute? :name, SoberSwag::Types::String.meta(description: <<~MARKDOWN, deprecated: true)
133
+ The given name of the students, with strings encoded as escaped-ASCII.
134
+ This is used by an internal Cobol microservice from 1968.
135
+ Please use unicode_name instead unless you are that microservice.
136
+ MARKDOWN
137
+ attribute? :unicode_name, SoberSwag::Types::String
138
+ end
139
+ ```
140
+
141
+ This will output the swagger you expect, with a description and a deprecated flag.
142
+
143
+ #### Adding Default Values
144
+
145
+ Sometimes it makes sense to specify a default value.
146
+ Don't worry, we've got you covered:
147
+
148
+ ```ruby
149
+ QueryInput = SoberSwag.input_object do
150
+ attribute :allow_first, SoberSwag::Types::Params::Bool.default(false) # smartly alters type-definition to establish that passing this is not required.
151
+ end
152
+ ```
153
+
108
154
  ## Special Thanks
109
155
 
110
- This gem is a mismatch of ideas from various sources.
156
+ This gem is a mishmash of ideas from various sources.
111
157
  The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
112
158
  On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
113
159
  The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/).
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby -W:no-experimental
1
+ #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'bundler/setup'
@@ -4,11 +4,11 @@ Serializers are a way to transform from one type to another.
4
4
  For example, you might want to change an ActiveRecord object to a JSON struct.
5
5
  You might also want to change an internal date-interval into a two-element array of dates, or some custom text format.
6
6
  You can do all of these things with SoberSwag serializers.
7
- Furthermore, Serializers document the *type* that they serialize, so you can use it to degenerate documentation.
7
+ Furthermore, Serializers document the *type* that they serialize, so you can use it to generate documentation.
8
8
 
9
9
  ## The Basics
10
10
 
11
- All serializers are inherted from [`SoberSwag::Serializer::Base`](../lib/sober_swag/serializer/base.rb).
11
+ All serializers are inherited from [`SoberSwag::Serializer::Base`](../lib/sober_swag/serializer/base.rb).
12
12
  This is an abstract class that implements several methods, most of which will be documented later.
13
13
  The two that are most interesting, however, are `#type` and `#serialize`.
14
14
 
@@ -55,12 +55,12 @@ In the future, we might add some "debug mode" sorta thing that will do type-chec
55
55
 
56
56
  ### Mapped
57
57
 
58
- Sometimes, you can create a serilaizer via a *proc*.
58
+ Sometimes, you can create a serializer via a *proc*.
59
59
  For example, let's say that I want a serializer that takes a `Date` and returns a string.
60
60
  I can do this:
61
61
 
62
62
  ```ruby
63
- date_string = SoberSwag::Serializer.Primitive(:String).via_map { |d| d.to_s }
63
+ date_string = SoberSwag::Serializer.primitive(:String).via_map { |d| d.to_s }
64
64
  ```
65
65
 
66
66
  This is implemented via [`SoberSwag::Serializer::Mapped`](../lib/sober_swag/serializer/mapped.rb).
@@ -87,7 +87,7 @@ my_serializer.optional.serialize(nil) # => nil
87
87
  # ^ nils become nil
88
88
  ```
89
89
 
90
- This properly changes the `type` to be a nillable type, as well.
90
+ This properly changes the `type` to be a nilable type, as well.
91
91
 
92
92
  ### Array
93
93
 
@@ -121,9 +121,9 @@ Let's define an output object:
121
121
 
122
122
  ```ruby
123
123
  StudentOutputObject = SoberSwag::OutputObject.define do
124
- field :first_name, Primitive(:String)
125
- field :last_name, Primitive(:String)
126
- field :recent_grades, Primitive(:Integer).array do |student|
124
+ field :first_name, primitive(:String)
125
+ field :last_name, primitive(:String)
126
+ field :recent_grades, primitive(:Integer).array do |student|
127
127
  student.graded_assignments.limit(100).pluck(:grade)
128
128
  end
129
129
  end
@@ -143,10 +143,10 @@ Let's take a look at their use:
143
143
 
144
144
  ```ruby
145
145
  StudentOutputObject = SoberSwag::OutputObject.define do
146
- field :first_name, Primitive(:String)
147
- field :last_name, Primitive(:String)
146
+ field :first_name, primitive(:String)
147
+ field :last_name, primitive(:String)
148
148
  view :detail do
149
- field :recent_grades, Primitive(:Integer).array do |student|
149
+ field :recent_grades, primitive(:Integer).array do |student|
150
150
  student.graded_assignments.limit(100).pluck(:grade)
151
151
  end
152
152
  end
@@ -189,7 +189,7 @@ StudentOutputObject = SoberSwag::OutputObject.define do
189
189
  end
190
190
  ```
191
191
 
192
- This can cause a circular dependecy.
192
+ This can cause a circular dependency.
193
193
  To break this, you can use a lambda:
194
194
 
195
195
  ```ruby
@@ -200,4 +200,4 @@ StudentOutputObject = SoberSwag::OutputObject.define do
200
200
  end
201
201
  ```
202
202
 
203
- For clarity (and to prevent infinitely-looping serializers on accident, we reccomend you *always* use an explicit view for dependent output objects.
203
+ For clarity (and to prevent infinitely-looping serializers on accident, we recommend you *always* use an explicit view for dependent output objects.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- sober_swag (0.4.0)
4
+ sober_swag (0.5.0)
5
5
  activesupport
6
6
  dry-struct (~> 1.0)
7
7
  dry-types (~> 1.2)
@@ -61,16 +61,18 @@ class PeopleController < ApplicationController
61
61
 
62
62
  define :get, :index, '/people/' do
63
63
  query_params do
64
- attribute? :first_name, Types::String
65
- attribute? :last_name, Types::String
66
- attribute? :view, Types::String.enum('base', 'detail')
64
+ attribute? :filters do
65
+ attribute? :first_name, Types::String
66
+ attribute? :last_name, Types::String
67
+ end
68
+ attribute :view, Types::String.default('base'.freeze).enum('base', 'detail')
67
69
  end
68
70
  response(:ok, 'all the people', PersonOutputObject.array)
69
71
  end
70
72
  def index
71
73
  @people = Person.all
72
- @people = @people.where('UPPER(first_name) LIKE UPPER(?)', "%#{parsed_query.first_name}%") if parsed_query.first_name
73
- @people = @people.where('UPPER(last_name) LIKE UPPER(?)', "%#{parsed_query.last_name}%") if parsed_query.last_name
74
+ @people = @people.where('UPPER(first_name) LIKE UPPER(?)', "%#{parsed_query.filters.first_name}%") if parsed_query.filters&.first_name
75
+ @people = @people.where('UPPER(last_name) LIKE UPPER(?)', "%#{parsed_query.filters.last_name}%") if parsed_query.filters&.last_name
74
76
  respond!(:ok, @people.includes(:posts), serializer_opts: { view: parsed_query.view })
75
77
  end
76
78
 
@@ -39,12 +39,20 @@ class PostsController < ApplicationController
39
39
  define :get, :index, '/posts/' do
40
40
  query_params do
41
41
  attribute? :view, ViewTypes
42
+ attribute :include_first, SoberSwag::Types::Params::Bool.default(false).meta(description: <<~MARKDOWN)
43
+ For historical reasons the first-ever post is the entire text of *Finnegan's Wake.*
44
+ Unfortunately, our contractors wound up depending on this quirk to complete dark arcane ceremonies,
45
+ so we can't remove it. Thus, by default, we don't include the first post unless you explicitly ask us to
46
+ (maybe you feel like some classic literature?).
47
+ MARKDOWN
42
48
  end
43
49
  response(:ok, 'all the posts', PostOutputObject.array)
44
50
  end
45
51
  def index
46
52
  @posts = Post.all
47
53
 
54
+ @posts = @posts.where('id > 1') unless parsed_query.include_first
55
+
48
56
  respond!(:ok, @posts.includes(:person), serializer_opts: { view: parsed_query.view })
49
57
  end
50
58
 
@@ -35,13 +35,13 @@ RSpec.describe 'Index action for people' do
35
35
  end
36
36
 
37
37
  context 'with a good first-name search' do
38
- let(:request) { get '/people', params: { first_name: 'A' } }
38
+ let(:request) { get '/people', params: { filters: { first_name: 'A' } } }
39
39
 
40
40
  it_behaves_like 'a request with the person'
41
41
  end
42
42
 
43
43
  context 'with a good last-name search' do
44
- let(:request) { get '/people', params: { last_name: 'G' } }
44
+ let(:request) { get '/people', params: { filters: { last_name: 'G' } } }
45
45
 
46
46
  it_behaves_like 'a request with the person'
47
47
  end
@@ -73,13 +73,16 @@ module SoberSwag
73
73
  end
74
74
 
75
75
  def path_schema
76
- path_schema_stub.map { |e| e.merge(in: :path) }
76
+ path_schema_stub.map do |e|
77
+ ensure_uncomplicated(e[:name], e[:schema])
78
+ e.merge(in: :path)
79
+ end
77
80
  rescue TooComplicatedError => e
78
81
  raise TooComplicatedForPathError, e.message
79
82
  end
80
83
 
81
84
  def query_schema
82
- path_schema_stub.map { |e| e.merge(in: :query) }
85
+ path_schema_stub.map { |e| e.merge(in: :query, style: :deepObject, explode: true) }
83
86
  rescue TooComplicatedError => e
84
87
  raise TooComplicatedForQueryError, e.message
85
88
  end
@@ -175,7 +178,7 @@ module SoberSwag
175
178
  end
176
179
  end
177
180
 
178
- def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
+ def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
179
182
  case object
180
183
  when Nodes::List
181
184
  {
@@ -239,13 +242,10 @@ module SoberSwag
239
242
  def path_schema_stub
240
243
  @path_schema_stub ||=
241
244
  object_schema[:properties].map do |k, v|
242
- ensure_uncomplicated(k, v)
245
+ # ensure_uncomplicated(k, v)
243
246
  {
244
247
  name: k,
245
248
  schema: v.reject { |key, _| %i[required nullable].include?(key) },
246
- # rubocop:disable Style/DoubleNegation
247
- allowEmptyValue: !object_schema[:required].include?(k) || !!v[:nullable], # if it's required, no empties, but if *nullabe*, empties are okay
248
- # rubocop:enable Style/DoubleNegation
249
249
  required: object_schema[:required].include?(k) || false
250
250
  }
251
251
  end
@@ -10,7 +10,7 @@ module SoberSwag
10
10
  ##
11
11
  # Given a symbol to this, we will use a primitive name
12
12
  def primitive(name)
13
- SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(name))
13
+ SoberSwag::Serializer.primitive(SoberSwag::Types.const_get(name))
14
14
  end
15
15
  end
16
16
  end
@@ -10,6 +10,9 @@ module SoberSwag
10
10
 
11
11
  def to_syntax # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
12
12
  case @node
13
+ when Dry::Types::Default
14
+ # we handle this elsewhere, so
15
+ bind(Parser.new(@node.type))
13
16
  when Dry::Types::Array::Member
14
17
  Nodes::List.new(bind(Parser.new(@node.member)))
15
18
  when Dry::Types::Enum
@@ -21,7 +24,7 @@ module SoberSwag
21
24
  when Dry::Types::Schema::Key
22
25
  Nodes::Attribute.new(
23
26
  @node.name,
24
- @node.required?,
27
+ @node.required? && !@node.type.default?,
25
28
  bind(Parser.new(@node.type))
26
29
  )
27
30
  when Dry::Types::Sum
@@ -18,7 +18,7 @@ module SoberSwag
18
18
  # in values raw.
19
19
  #
20
20
  # @param contained {Class} Dry::Type to use
21
- def Primitive(contained) # rubocop:disable Naming/MethodName
21
+ def primitive(contained)
22
22
  SoberSwag::Serializer::Primitive.new(contained)
23
23
  end
24
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sober_swag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Super
@@ -166,7 +166,6 @@ files:
166
166
  - ".ruby-version"
167
167
  - ".travis.yml"
168
168
  - Gemfile
169
- - Gemfile.lock
170
169
  - LICENSE.txt
171
170
  - README.md
172
171
  - Rakefile
@@ -1,116 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- sober_swag (0.3.0)
5
- activesupport
6
- dry-struct (~> 1.0)
7
- dry-types (~> 1.2)
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- activesupport (6.0.3.2)
13
- concurrent-ruby (~> 1.0, >= 1.0.2)
14
- i18n (>= 0.7, < 2)
15
- minitest (~> 5.1)
16
- tzinfo (~> 1.1)
17
- zeitwerk (~> 2.2, >= 2.2.2)
18
- ast (2.4.1)
19
- coderay (1.1.2)
20
- concurrent-ruby (1.1.6)
21
- diff-lcs (1.4.4)
22
- docile (1.3.2)
23
- dry-configurable (0.11.6)
24
- concurrent-ruby (~> 1.0)
25
- dry-core (~> 0.4, >= 0.4.7)
26
- dry-equalizer (~> 0.2)
27
- dry-container (0.7.2)
28
- concurrent-ruby (~> 1.0)
29
- dry-configurable (~> 0.1, >= 0.1.3)
30
- dry-core (0.4.9)
31
- concurrent-ruby (~> 1.0)
32
- dry-equalizer (0.3.0)
33
- dry-inflector (0.2.0)
34
- dry-logic (1.0.6)
35
- concurrent-ruby (~> 1.0)
36
- dry-core (~> 0.2)
37
- dry-equalizer (~> 0.2)
38
- dry-struct (1.3.0)
39
- dry-core (~> 0.4, >= 0.4.4)
40
- dry-equalizer (~> 0.3)
41
- dry-types (~> 1.3)
42
- ice_nine (~> 0.11)
43
- dry-types (1.4.0)
44
- concurrent-ruby (~> 1.0)
45
- dry-container (~> 0.3)
46
- dry-core (~> 0.4, >= 0.4.4)
47
- dry-equalizer (~> 0.3)
48
- dry-inflector (~> 0.1, >= 0.1.2)
49
- dry-logic (~> 1.0, >= 1.0.2)
50
- i18n (1.8.3)
51
- concurrent-ruby (~> 1.0)
52
- ice_nine (0.11.2)
53
- method_source (0.9.2)
54
- minitest (5.14.1)
55
- parallel (1.19.2)
56
- parser (2.7.1.4)
57
- ast (~> 2.4.1)
58
- pry (0.12.2)
59
- coderay (~> 1.1.0)
60
- method_source (~> 0.9.0)
61
- rainbow (3.0.0)
62
- rake (13.0.1)
63
- regexp_parser (1.7.1)
64
- rexml (3.2.4)
65
- rspec (3.9.0)
66
- rspec-core (~> 3.9.0)
67
- rspec-expectations (~> 3.9.0)
68
- rspec-mocks (~> 3.9.0)
69
- rspec-core (3.9.2)
70
- rspec-support (~> 3.9.3)
71
- rspec-expectations (3.9.2)
72
- diff-lcs (>= 1.2.0, < 2.0)
73
- rspec-support (~> 3.9.0)
74
- rspec-mocks (3.9.1)
75
- diff-lcs (>= 1.2.0, < 2.0)
76
- rspec-support (~> 3.9.0)
77
- rspec-support (3.9.3)
78
- rubocop (0.88.0)
79
- parallel (~> 1.10)
80
- parser (>= 2.7.1.1)
81
- rainbow (>= 2.2.2, < 4.0)
82
- regexp_parser (>= 1.7)
83
- rexml
84
- rubocop-ast (>= 0.1.0, < 1.0)
85
- ruby-progressbar (~> 1.7)
86
- unicode-display_width (>= 1.4.0, < 2.0)
87
- rubocop-ast (0.1.0)
88
- parser (>= 2.7.0.1)
89
- rubocop-rspec (1.42.0)
90
- rubocop (>= 0.87.0)
91
- ruby-progressbar (1.10.1)
92
- simplecov (0.18.5)
93
- docile (~> 1.1)
94
- simplecov-html (~> 0.11)
95
- simplecov-html (0.12.2)
96
- thread_safe (0.3.6)
97
- tzinfo (1.2.7)
98
- thread_safe (~> 0.1)
99
- unicode-display_width (1.7.0)
100
- zeitwerk (2.4.0)
101
-
102
- PLATFORMS
103
- ruby
104
-
105
- DEPENDENCIES
106
- bundler (~> 2.0)
107
- pry
108
- rake (~> 13.0)
109
- rspec (~> 3.0)
110
- rubocop
111
- rubocop-rspec
112
- simplecov
113
- sober_swag!
114
-
115
- BUNDLED WITH
116
- 2.1.4