sober_swag 0.1.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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +33 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +7 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +92 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +7 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +38 -0
  14. data/bin/setup +8 -0
  15. data/example/.gitignore +24 -0
  16. data/example/.ruby-version +1 -0
  17. data/example/Gemfile +42 -0
  18. data/example/Gemfile.lock +212 -0
  19. data/example/README.md +24 -0
  20. data/example/Rakefile +6 -0
  21. data/example/app/controllers/application_controller.rb +2 -0
  22. data/example/app/controllers/concerns/.keep +0 -0
  23. data/example/app/controllers/people_controller.rb +74 -0
  24. data/example/app/jobs/application_job.rb +7 -0
  25. data/example/app/models/application_record.rb +3 -0
  26. data/example/app/models/concerns/.keep +0 -0
  27. data/example/app/models/person.rb +2 -0
  28. data/example/bin/bundle +114 -0
  29. data/example/bin/rails +9 -0
  30. data/example/bin/rake +9 -0
  31. data/example/bin/setup +33 -0
  32. data/example/bin/spring +17 -0
  33. data/example/config/application.rb +37 -0
  34. data/example/config/boot.rb +4 -0
  35. data/example/config/credentials.yml.enc +1 -0
  36. data/example/config/database.yml +25 -0
  37. data/example/config/environment.rb +5 -0
  38. data/example/config/environments/development.rb +44 -0
  39. data/example/config/environments/production.rb +91 -0
  40. data/example/config/environments/test.rb +38 -0
  41. data/example/config/initializers/application_controller_renderer.rb +8 -0
  42. data/example/config/initializers/backtrace_silencers.rb +7 -0
  43. data/example/config/initializers/cors.rb +16 -0
  44. data/example/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/example/config/initializers/inflections.rb +16 -0
  46. data/example/config/initializers/mime_types.rb +4 -0
  47. data/example/config/initializers/wrap_parameters.rb +14 -0
  48. data/example/config/locales/en.yml +33 -0
  49. data/example/config/puma.rb +38 -0
  50. data/example/config/routes.rb +6 -0
  51. data/example/config/spring.rb +6 -0
  52. data/example/config.ru +5 -0
  53. data/example/db/migrate/20200311152021_create_people.rb +12 -0
  54. data/example/db/schema.rb +23 -0
  55. data/example/db/seeds.rb +7 -0
  56. data/example/lib/tasks/.keep +0 -0
  57. data/example/log/.keep +0 -0
  58. data/example/person.json +4 -0
  59. data/example/public/robots.txt +1 -0
  60. data/example/test/controllers/.keep +0 -0
  61. data/example/test/fixtures/.keep +0 -0
  62. data/example/test/fixtures/files/.keep +0 -0
  63. data/example/test/fixtures/people.yml +11 -0
  64. data/example/test/integration/.keep +0 -0
  65. data/example/test/models/.keep +0 -0
  66. data/example/test/models/person_test.rb +7 -0
  67. data/example/test/test_helper.rb +13 -0
  68. data/example/tmp/.keep +0 -0
  69. data/example/vendor/.keep +0 -0
  70. data/lib/sober_swag/blueprint/field.rb +36 -0
  71. data/lib/sober_swag/blueprint/field_syntax.rb +17 -0
  72. data/lib/sober_swag/blueprint/view.rb +44 -0
  73. data/lib/sober_swag/blueprint.rb +113 -0
  74. data/lib/sober_swag/compiler/error.rb +5 -0
  75. data/lib/sober_swag/compiler/path.rb +80 -0
  76. data/lib/sober_swag/compiler/paths.rb +54 -0
  77. data/lib/sober_swag/compiler/type.rb +235 -0
  78. data/lib/sober_swag/compiler.rb +107 -0
  79. data/lib/sober_swag/controller/route.rb +136 -0
  80. data/lib/sober_swag/controller/undefined_body_error.rb +6 -0
  81. data/lib/sober_swag/controller/undefined_path_error.rb +6 -0
  82. data/lib/sober_swag/controller/undefined_query_error.rb +6 -0
  83. data/lib/sober_swag/controller.rb +157 -0
  84. data/lib/sober_swag/nodes/array.rb +30 -0
  85. data/lib/sober_swag/nodes/attribute.rb +31 -0
  86. data/lib/sober_swag/nodes/base.rb +51 -0
  87. data/lib/sober_swag/nodes/binary.rb +44 -0
  88. data/lib/sober_swag/nodes/enum.rb +28 -0
  89. data/lib/sober_swag/nodes/list.rb +40 -0
  90. data/lib/sober_swag/nodes/nullable_primitive.rb +6 -0
  91. data/lib/sober_swag/nodes/object.rb +12 -0
  92. data/lib/sober_swag/nodes/one_of.rb +12 -0
  93. data/lib/sober_swag/nodes/primitive.rb +29 -0
  94. data/lib/sober_swag/nodes/sum.rb +6 -0
  95. data/lib/sober_swag/nodes.rb +20 -0
  96. data/lib/sober_swag/parser.rb +73 -0
  97. data/lib/sober_swag/path/integer.rb +21 -0
  98. data/lib/sober_swag/path/lit.rb +41 -0
  99. data/lib/sober_swag/path/literal.rb +29 -0
  100. data/lib/sober_swag/path/param.rb +33 -0
  101. data/lib/sober_swag/path.rb +8 -0
  102. data/lib/sober_swag/serializer/array.rb +21 -0
  103. data/lib/sober_swag/serializer/base.rb +38 -0
  104. data/lib/sober_swag/serializer/conditional.rb +49 -0
  105. data/lib/sober_swag/serializer/field_list.rb +44 -0
  106. data/lib/sober_swag/serializer/mapped.rb +29 -0
  107. data/lib/sober_swag/serializer/optional.rb +29 -0
  108. data/lib/sober_swag/serializer/primitive.rb +15 -0
  109. data/lib/sober_swag/serializer.rb +23 -0
  110. data/lib/sober_swag/types.rb +5 -0
  111. data/lib/sober_swag/version.rb +5 -0
  112. data/lib/sober_swag.rb +29 -0
  113. data/sober_swag.gemspec +40 -0
  114. metadata +269 -0
@@ -0,0 +1,29 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # Root node of the tree
5
+ class Primitive < Base
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+
10
+ attr_reader :value
11
+
12
+ def map(&block)
13
+ self.class.new(block.call(value))
14
+ end
15
+
16
+ def deconstruct
17
+ [value]
18
+ end
19
+
20
+ def deconstruct_keys(_)
21
+ { value: value }
22
+ end
23
+
24
+ def cata(&block)
25
+ block.call(self.class.new(value))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ class Sum < Binary
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ module SoberSwag
2
+ ##
3
+ # Base namespace for all nodes.
4
+ # These nodes are compiled into an actual swagger definition
5
+ # via a catamorphism, which I promise is not nearly as scary as it sounds.
6
+ # Sort of.
7
+ module Nodes
8
+ autoload :Base, 'sober_swag/nodes/base'
9
+ autoload :Enum, 'sober_swag/nodes/enum'
10
+ autoload :Binary, 'sober_swag/nodes/binary'
11
+ autoload :Primitive, 'sober_swag/nodes/primitive'
12
+ autoload :NullablePrimitive, 'sober_swag/nodes/nullable_primitive'
13
+ autoload :Sum, 'sober_swag/nodes/sum'
14
+ autoload :Array, 'sober_swag/nodes/array'
15
+ autoload :Object, 'sober_swag/nodes/object'
16
+ autoload :Attribute, 'sober_swag/nodes/attribute'
17
+ autoload :OneOf, 'sober_swag/nodes/one_of'
18
+ autoload :List, 'sober_swag/nodes/list'
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ module SoberSwag
2
+ class Parser
3
+ def initialize(node)
4
+ @node = node
5
+ @found = Set.new
6
+ end
7
+
8
+ def to_syntax
9
+ case @node
10
+ when Dry::Types::Array::Member
11
+ Nodes::List.new(bind(Parser.new(@node.member)))
12
+ when Dry::Types::Enum
13
+ Nodes::Enum.new(@node.values)
14
+ when Dry::Types::Schema
15
+ Nodes::Object.new(
16
+ @node.map { |attr| bind(Parser.new(attr)) }
17
+ )
18
+ when Dry::Types::Schema::Key
19
+ Nodes::Attribute.new(
20
+ @node.name,
21
+ @node.required?,
22
+ bind(Parser.new(@node.type))
23
+ )
24
+ when Dry::Types::Sum
25
+ left = bind(Parser.new(@node.left))
26
+ right = bind(Parser.new(@node.right))
27
+ # special case booleans to just return the left value
28
+ # this is because modeling a boolean as a sum type of
29
+ # TrueClass and FalseClass is kinda weird, because they're
30
+ # considered different types instead of different constructors,
31
+ # which we don't want to do
32
+ is_bool = [left, right].all? do |e|
33
+ e.respond_to?(:value) && [TrueClass, FalseClass].include?(e.value)
34
+ end
35
+ if is_bool
36
+ left
37
+ else
38
+ Nodes::Sum.new(left, right)
39
+ end
40
+ when Dry::Types::Constrained
41
+ bind(Parser.new(@node.type))
42
+ when Dry::Types::Nominal
43
+ # start off with the moral equivalent of NodeTree[String]
44
+ Nodes::Primitive.new(@node.primitive)
45
+ else
46
+ # Inside of this case we have a class that is some user-defined type
47
+ # We put it in our array of found types, and consider it a primitive
48
+ @found.add(@node)
49
+ Nodes::Primitive.new(@node)
50
+ end
51
+ end
52
+
53
+ def run_parser
54
+ [to_syntax, found]
55
+ end
56
+
57
+ ##
58
+ # What other types did we find while parsing this type?
59
+ attr_reader :found
60
+
61
+ ##
62
+ # Call `.to_syntax` on another node, putting any new classes it finds
63
+ # in the list of classes we found in the process.
64
+ #
65
+ # If you're a big Haskell nerd, then this is >>=.
66
+ def bind(other)
67
+ result = other.to_syntax
68
+ @found += other.found
69
+ result
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ module SoberSwag
2
+ module Path
3
+ class Integer < Node
4
+
5
+ def initialize; end;
6
+
7
+ def jumpable?
8
+ true
9
+ end
10
+
11
+ def param?
12
+ true
13
+ end
14
+
15
+ def param_type
16
+ SoberSwag::Types::Paramter::Integer
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module SoberSwag
2
+ class Path
3
+ class Lit
4
+ ##
5
+ # Parse a literal path fragment
6
+ def initialize(lit)
7
+ @lit = lit
8
+ end
9
+
10
+ attr_reader :lit
11
+
12
+ def param?
13
+ false
14
+ end
15
+
16
+ def param_type
17
+ nil
18
+ end
19
+
20
+ def param_key
21
+ nil
22
+ end
23
+
24
+ ##
25
+ # Constant to avoid a bunch of array allocation
26
+ MATCH_SUCC = [:match, nil].freeze
27
+ ##
28
+ # Constant to avoid a bunch of array allocation
29
+ MATHC_FAIL = [:fail].freeze
30
+
31
+ def match(param)
32
+ if param == lit
33
+ MATCH_SUCC
34
+ else
35
+ MATCH_FAIL
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module SoberSwag
2
+ module Path
3
+ ##
4
+ # One literal text fragment, basically
5
+ class Literal < Node
6
+ ##
7
+ # Make a new text node
8
+ # @param text [String]
9
+ def initialize(text)
10
+ @text = text
11
+ end
12
+
13
+ attr_reader :text
14
+
15
+ ##
16
+ # We can make a jump table out of this node!
17
+ def jumpable?
18
+ true
19
+ end
20
+
21
+ ##
22
+ # This doesn't read a parameter type.
23
+ def param?
24
+ false
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ module SoberSwag
2
+ class Path
3
+ ##
4
+ # Parse a parameter
5
+ class Param
6
+ def initialize(name, type)
7
+ @name = name
8
+ @type = type
9
+ end
10
+
11
+ def param?
12
+ true
13
+ end
14
+
15
+ def param_key
16
+ @name
17
+ end
18
+
19
+ def param_type
20
+ @type
21
+ end
22
+
23
+ def match(param)
24
+ if (m = @type.try(param)).success?
25
+ [:match, m]
26
+ else
27
+ [:fail]
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ module SoberSwag
2
+ class Path
3
+
4
+ autoload(:Lit, 'sober_swag/path/lit')
5
+ autoload(:Param, 'sober_swag/path/param')
6
+
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ ##
4
+ # Make a serialize of arrays out of a serializer of the elements
5
+ class Array < Base
6
+ def initialize(element_serializer)
7
+ @element_serializer = element_serializer
8
+ end
9
+
10
+ attr_reader :element_serializer
11
+
12
+ def serialize(object, options = {})
13
+ object.map { |a| element_serializer.serialize(a, options) }
14
+ end
15
+
16
+ def type
17
+ SoberSwag::Types::Array.of(element_serializer.type)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ class Base
4
+
5
+ ##
6
+ # Return a new serializer that is an *array* of elements of this serializer.
7
+ def array
8
+ SoberSwag::Serializer::Array.new(self)
9
+ end
10
+
11
+ ##
12
+ # Returns a serializer that will pass `nil` values on unscathed
13
+ def optional
14
+ SoberSwag::Serializer::Optional.new(self)
15
+ end
16
+
17
+ ##
18
+ # If I am a serializer for type 'a', and you give me a way to turn 'a's into 'b's,
19
+ # I can give you a serializer for type 'b' by running the funciton you gave.
20
+ # For example, if I am a serializer for {String}, and you know how to turn
21
+ # an {Int} into a {String}, I can now serialize {Int}s (by turning them into a string).
22
+ #
23
+ # Note that the *declared* type of this is *not* changed: from a user's perspective,
24
+ # they see a "string"
25
+ def via_map(&block)
26
+ SoberSwag::Serializer::Mapped.new(self, block)
27
+ end
28
+
29
+ ##
30
+ # Serializer lets you get a serializer from things that might be classes
31
+ # because of the blueprint naming hack.
32
+ def serializer
33
+ self
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ ##
4
+ # Conditionally serialize one thing *or* the other thing via deciding on a condition.
5
+ # This works by taking three elements: a "decision" proc, a "left" serializer, and a "right" serializer.
6
+ # The decision proc takes in both the object to be serialized *and* the options hash, and returns a
7
+ # `[:left, val]` object, or a `[:right, val]` object, which
8
+ # then get passed on to the appropriate serializer.
9
+ #
10
+ # This is a very weird, not-very-Ruby-like abstraction, *upon which* we can build abstractions that are actually use for users.
11
+ # It lets you build abstractions like "Use this serializer if a type has this class, otherwise use this other one."
12
+ # When composed together, you can make arbitrary decision trees.
13
+ class Conditional < Base
14
+ ##
15
+ # Error thrown when a chooser proc returns a non left-or-right value.
16
+ class BadChoiceError < Error; end
17
+
18
+ def initialize(chooser, left, right)
19
+ @chooser = chooser
20
+ @left = left
21
+ @right = right
22
+ end
23
+
24
+ attr_reader :chooser, :left, :right
25
+
26
+ def serialize(object, options = {})
27
+ tag, val = chooser.call(object, options)
28
+ if tag == :left
29
+ left.serialize(val, options)
30
+ elsif tag == :right
31
+ right.serialize(val, options)
32
+ else
33
+ raise BadChoiceError, "result of chooser proc was not a left or right, but a #{val.class}"
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Since this could potentially serialize one of two alternatives,
39
+ # the "type" we serialize two is *either* one alternative or the other.
40
+ def type
41
+ if left.type == right.type
42
+ left.type
43
+ else
44
+ left.type | right.type
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,44 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ ##
4
+ # Extract out a hash from a list of
5
+ # name/serializer pairs.
6
+ class FieldList < Base
7
+
8
+ def initialize(field_list)
9
+ @field_list = field_list
10
+ end
11
+
12
+ attr_reader :field_list
13
+
14
+ ##
15
+ # Alias to make writing primitive stuff much easier
16
+ def primitive(symbol)
17
+ SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(symbol))
18
+ end
19
+
20
+
21
+ def serialize(object, options)
22
+ field_list.map { |field|
23
+ [field.name, field.serializer.serialize(object, options)]
24
+ }.to_h
25
+ end
26
+
27
+ def type
28
+ @type ||= make_struct_type!
29
+ end
30
+
31
+ private
32
+
33
+ def make_struct_type!
34
+ f = field_list
35
+ Class.new(Dry::Struct) do
36
+ f.each do |field|
37
+ attribute field.name, field.serializer.type
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ ##
4
+ # A new serializer by mapping over the serialization function
5
+ class Mapped < Base
6
+
7
+ def initialize(base, map_f)
8
+ @base = base
9
+ @map_f = map_f
10
+ end
11
+
12
+ def serialize(object, options)
13
+ @base.serialize(@map_f.call(object), options)
14
+ end
15
+
16
+ def type
17
+ @base.type
18
+ end
19
+
20
+ ##
21
+ # I have no freaking clue if ruby optimizes proc composition,
22
+ # but we at least save some node traversals here
23
+ def via_map(&block)
24
+ SoberSwag::Serializer::Mapped.new(@base, @map_f >> block)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ class Optional < Base
4
+
5
+ def initialize(inner)
6
+ @inner = inner
7
+ end
8
+
9
+ attr_reader :inner
10
+
11
+ def serialize(object, options = {})
12
+ if object.nil?
13
+ object
14
+ else
15
+ inner.serialize(object, options)
16
+ end
17
+ end
18
+
19
+ def type
20
+ inner.type.optional
21
+ end
22
+
23
+ def optional(*)
24
+ raise ArgumentError, 'no nesting optionals please'
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ class Primitive < Base
4
+ def initialize(type)
5
+ @type = type
6
+ end
7
+
8
+ attr_reader :type
9
+
10
+ def serialize(object, options = {})
11
+ object
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module SoberSwag
2
+ module Serializer
3
+ autoload(:Base, 'sober_swag/serializer/base')
4
+ autoload(:Primitive, 'sober_swag/serializer/primitive')
5
+ autoload(:Conditional, 'sober_swag/serializer/conditional')
6
+ autoload(:Array, 'sober_swag/serializer/array')
7
+ autoload(:Mapped, 'sober_swag/serializer/mapped')
8
+ autoload(:Optional, 'sober_swag/serializer/optional')
9
+ autoload(:FieldList, 'sober_swag/serializer/field_list')
10
+
11
+ class << self
12
+ ##
13
+ # Use a "Primitive" serializer, which *does not* actually do any type-changing, and instead passes
14
+ # in values raw.
15
+ #
16
+ # @param contained {Class} Dry::Type to use
17
+ def Primitive(contained)
18
+ SoberSwag::Serializer::Primitive.new(contained)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module SoberSwag
2
+ class Types
3
+ include ::Dry::Types()
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SoberSwag
4
+ VERSION = '0.1.0'
5
+ end
data/lib/sober_swag.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ Bundler.setup
5
+ require 'dry-struct'
6
+ require 'dry-types'
7
+ require 'sober_swag/types'
8
+ require 'sober_swag/version'
9
+ require 'active_support/inflector'
10
+
11
+ ##
12
+ # Root namespace
13
+ module SoberSwag
14
+ class Error < StandardError; end
15
+
16
+ autoload :Parser, 'sober_swag/parser'
17
+ autoload :Serializer, 'sober_swag/serializer'
18
+ autoload :Blueprint, 'sober_swag/blueprint'
19
+ autoload :Nodes, 'sober_swag/nodes'
20
+ autoload :Compiler, 'sober_swag/compiler'
21
+ autoload :Controller, 'sober_swag/controller'
22
+
23
+ ##
24
+ # Define a struct of something.
25
+ # Useful to prevent weirdness from autoloading.
26
+ def self.struct(parent = Dry::Struct, &block)
27
+ Class.new(parent, &block)
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'sober_swag/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'sober_swag'
9
+ spec.version = SoberSwag::VERSION
10
+ spec.authors = ['Anthony Super']
11
+ spec.email = ['asuper@sondermind.com']
12
+
13
+ spec.summary = 'Generate swagger types from dry-types'
14
+ spec.description = 'Parse data, don\'t write docs'
15
+ spec.homepage = 'https://github.com/SonderMindOrg/sober_swag'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'dry-struct', '~> 1.0'
32
+ spec.add_dependency 'dry-types', '~> 1.2'
33
+ spec.add_dependency 'activesupport'
34
+
35
+ spec.add_development_dependency 'pry'
36
+ spec.add_development_dependency 'simplecov'
37
+ spec.add_development_dependency 'bundler', '~> 2.0'
38
+ spec.add_development_dependency 'rake', '~> 13.0'
39
+ spec.add_development_dependency 'rspec', '~> 3.0'
40
+ end