sober_swag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
data/example/log/.keep ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ {
2
+ "first_name": "Tony",
3
+ "last_name": "Super"
4
+ }
@@ -0,0 +1 @@
1
+ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
File without changes
File without changes
File without changes
@@ -0,0 +1,11 @@
1
+ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2
+
3
+ one:
4
+ first_name: MyText
5
+ last_name: MyText
6
+ date_of_birth: 2020-03-11 09:20:21
7
+
8
+ two:
9
+ first_name: MyText
10
+ last_name: MyText
11
+ date_of_birth: 2020-03-11 09:20:21
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class PersonTest < ActiveSupport::TestCase
4
+ # test "the truth" do
5
+ # assert true
6
+ # end
7
+ end
@@ -0,0 +1,13 @@
1
+ ENV['RAILS_ENV'] ||= 'test'
2
+ require_relative '../config/environment'
3
+ require 'rails/test_help'
4
+
5
+ class ActiveSupport::TestCase
6
+ # Run tests in parallel with specified workers
7
+ parallelize(workers: :number_of_processors)
8
+
9
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
10
+ fixtures :all
11
+
12
+ # Add more helper methods to be used by all tests here...
13
+ end
data/example/tmp/.keep ADDED
File without changes
File without changes
@@ -0,0 +1,36 @@
1
+ module SoberSwag
2
+ class Blueprint
3
+ class Field
4
+ def initialize(name, serializer, from: nil, &block)
5
+ @name = name
6
+ @root_serializer = serializer
7
+ @from = from
8
+ @block = block
9
+ end
10
+
11
+ attr_reader :name
12
+
13
+ def serializer
14
+ @serializer ||= @root_serializer.serializer.via_map(&transform_proc)
15
+ end
16
+
17
+ private
18
+
19
+ def transform_proc
20
+ if @block
21
+ @block
22
+ else
23
+ key = @from || @name
24
+ proc do |object, _|
25
+ if object.respond_to?(key)
26
+ object.public_send(key)
27
+ else
28
+ object[key]
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ module SoberSwag
2
+ class Blueprint
3
+ ##
4
+ # Syntax for definitions that can add fields.
5
+ module FieldSyntax
6
+ def field(name, serializer, from: nil, &block)
7
+ add_field!(Field.new(name, serializer, from: from, &block))
8
+ end
9
+
10
+ ##
11
+ # Given a symbol to this, we will use a primitive name
12
+ def primitive(name)
13
+ SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(name))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ module SoberSwag
2
+ class Blueprint
3
+ class View
4
+
5
+ def self.define(name, base_fields, &block)
6
+ self.new(name, base_fields).tap do |view|
7
+ view.instance_eval(&block)
8
+ end
9
+ end
10
+
11
+ class NestingError < Error; end;
12
+
13
+ include FieldSyntax
14
+
15
+ def initialize(name, base_fields = [])
16
+ @name = name
17
+ @fields = base_fields.dup
18
+ end
19
+
20
+ attr_reader :name, :fields
21
+
22
+ def except!(name)
23
+ @fields.select! { |f| f.name != name }
24
+ end
25
+
26
+ def view(*)
27
+ raise NestingError, 'no views in views'
28
+ end
29
+
30
+ def add_field!(field)
31
+ @fields << field
32
+ end
33
+
34
+ ##
35
+ # Get the serializer defined by this view.
36
+ # WARNING: Don't add more fields after you call this.
37
+ def serializer
38
+ @serializer ||=
39
+ SoberSwag::Serializer::FieldList.new(fields)
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,113 @@
1
+ require 'sober_swag/serializer'
2
+
3
+ module SoberSwag
4
+ ##
5
+ # Create a serializer that is heavily inspired by the "Blueprinter" library.
6
+ # This allows you to make "views" and such inside.
7
+ #
8
+ # Under the hood, this is actually all based on {SoberSwag::Serialzier::Base}.
9
+ class Blueprint
10
+ autoload(:Field, 'sober_swag/blueprint/field')
11
+ autoload(:FieldSyntax, 'sober_swag/blueprint/field_syntax')
12
+ autoload(:View, 'sober_swag/blueprint/view')
13
+
14
+ ##
15
+ # Use a Blueprint to define a new serializer.
16
+ # It will be based on {SoberSwag::Serializer::Base}.
17
+ #
18
+ # An example is illustrative:
19
+ #
20
+ # PersonSerializer = SoberSwag::Blueprint.define do
21
+ # field :id, primitive(:Integer)
22
+ # field :name, primtive(:String).optional
23
+ #
24
+ # view :complex do
25
+ # field :age, primitive(:Integer)
26
+ # field :title, primitive(:String)
27
+ # end
28
+ # end
29
+ #
30
+ # Note: This currently will generate a new *class* that does serialization.
31
+ # However, this is only a hack to get rid of the weird naming issue when
32
+ # generating swagger from dry structs: their section of the schema area
33
+ # is defined by their *Ruby Class Name*. In the future, if we get rid of this,
34
+ # we might be able to keep this on the value-level, in which case {#define}
35
+ # can simply return an *instance* of SoberSwag::Serializer that does
36
+ # the correct thing, with the name you give it. This works for now, though.
37
+ def self.define(&block)
38
+ self.new.tap { |o|
39
+ o.instance_eval(&block)
40
+ }.serializer_class
41
+ end
42
+
43
+ def initialize(base_fields = [])
44
+ @fields = base_fields.dup
45
+ @views = []
46
+ end
47
+
48
+ attr_reader :fields, :views
49
+
50
+ include FieldSyntax
51
+
52
+ def add_field!(field)
53
+ @fields << field
54
+ end
55
+
56
+ def view(name, &block)
57
+ @views << View.define(name, fields, &block)
58
+ end
59
+
60
+ ##
61
+ # Instead of generating a new value-level construct,
62
+ # this will generate a new *class*. This is so that you can
63
+ # name this blueprint, and have the views named as sub-classes.
64
+ # This is important as the type compiler uses class names
65
+ # for generating refs.
66
+ def serializer_class
67
+ @serializer_class ||= make_serializer_class!
68
+ end
69
+
70
+ private
71
+
72
+ def make_serializer_class!
73
+ # Klass we'll use
74
+ klass = Class.new(SoberSwag::Serializer::Base)
75
+ # The actual serialization logic is defined in a field list
76
+ base_serializer = SoberSwag::Serializer::FieldList.new(fields)
77
+ # WhateverBlueprint::Base is now used as the name for a ref
78
+ klass.const_set(:Base, base_serializer.type)
79
+ final_serializer = views.reduce(base_serializer) do |base, view|
80
+ view_serializer = view.serializer
81
+ # If we have a view :foo, its type is named
82
+ # WhateverBlueprint::Foo
83
+ klass.const_set(view.name.to_s.classify, view_serializer.type)
84
+ SoberSwag::Serializer::Conditional.new(
85
+ proc do |object, options|
86
+ if options[:view].to_s == view.name.to_s
87
+ [:left, object]
88
+ else
89
+ [:right, object]
90
+ end
91
+ end,
92
+ view_serializer,
93
+ base
94
+ )
95
+ end
96
+ klass.send(:define_method, :serialize) do |object, options = {}|
97
+ final_serializer.serialize(object, options)
98
+ end
99
+ klass.send(:define_method, :type) do
100
+ final_serializer.type
101
+ end
102
+ klass.send(:define_singleton_method, :type) do
103
+ final_serializer.type
104
+ end
105
+ klass.send(:define_singleton_method, :serializer) do
106
+ klass.new
107
+ end
108
+
109
+ klass
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,5 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ class Error < ::SoberSwag::Error; end
4
+ end
5
+ end
@@ -0,0 +1,80 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ ##
4
+ # Compile a singular path, and that's it.
5
+ # Only handles the actual body.
6
+ class Path
7
+ ##
8
+ # @param route [SoberSwag::Controller::Route] a route to use
9
+ # @param compiler [SoberSwag::Compiler] the compiler to use for type compilation
10
+ def initialize(route, compiler)
11
+ @route = route
12
+ @compiler = compiler
13
+ end
14
+
15
+ attr_reader :route, :compiler
16
+
17
+ def schema
18
+ base = {}
19
+ base[:summary] = route.summary if route.summary
20
+ base[:description] = route.description if route.description
21
+ base[:parameters] = params if params.any?
22
+ base[:responses] = responses
23
+ base[:requestBody] = request_body if request_body
24
+ base
25
+ end
26
+
27
+ def responses
28
+ route.response_serializers.map { |status, serializer|
29
+ [
30
+ status.to_s,
31
+ {
32
+ description: route.response_descriptions[status],
33
+ content: {
34
+ 'application/json': {
35
+ schema: compiler.response_for(
36
+ serializer.respond_to?(:new) ? serializer.new.type : serializer.type
37
+ )
38
+ }
39
+ }
40
+ }
41
+ ]
42
+ }.to_h
43
+ end
44
+
45
+ def params
46
+ query_params + path_params
47
+ end
48
+
49
+ def query_params
50
+ if route.query_params_class
51
+ compiler.query_params_for(route.query_params_class)
52
+ else
53
+ []
54
+ end
55
+ end
56
+
57
+ def path_params
58
+ if route.path_params_class
59
+ compiler.path_params_for(route.path_params_class)
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def request_body
66
+ return nil unless route.request_body_class
67
+
68
+ {
69
+ required: true,
70
+ content: {
71
+ 'application/json': {
72
+ schema: compiler.body_for(route.request_body_class)
73
+ }
74
+ }
75
+ }
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,54 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ ##
4
+ # Compile multiple routes into a paths set.
5
+ class Paths
6
+ def initialize
7
+ @routes = []
8
+ end
9
+
10
+ def add_route(route)
11
+ @routes << route
12
+ end
13
+
14
+ def grouped_paths
15
+ routes.group_by(&:path)
16
+ end
17
+
18
+ ##
19
+ # Slightly weird method that gives you a compiled
20
+ # paths list. Since this is only a compiler for paths,
21
+ # it has *no idea* how to handle types. So, it takes a compiler
22
+ # which it will use to do that for it.
23
+ def paths_list(compiler)
24
+ grouped_paths.transform_values do |values|
25
+ values.map { |route|
26
+ [route.method, compile_route(route, compiler)]
27
+ }.to_h
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Get a list of all types we discovered when compiling
33
+ # the paths.
34
+ def found_types
35
+ return enum_for(:found_types) unless block_given?
36
+
37
+ routes.each do |route|
38
+ %i[body_class query_class path_params_class].each do |k|
39
+ yield route.public_send(k) if route.public_send(k)
40
+ end
41
+ end
42
+ end
43
+
44
+ attr_reader :routes
45
+
46
+ private
47
+
48
+ def compile_route(route, compiler)
49
+ SoberSwag::Compiler::Path.new(route, compiler).schema
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,235 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ ##
4
+ # A compiler for DRY-Struct data types, essentially.
5
+ # It only consumes one type at a time.
6
+ class Type
7
+ class << self
8
+ def get_ref(klass)
9
+ "#/components/schemas/#{safe_name(klass)}"
10
+ end
11
+
12
+ def safe_name(klass)
13
+ klass.to_s.gsub('::', '.')
14
+ end
15
+
16
+ def primitive?(value)
17
+ primitive_def(value) != nil
18
+ end
19
+
20
+ def primitive_def(value)
21
+ return nil unless value.is_a?(Class)
22
+
23
+ if (name = primitive_name(value))
24
+ { type: name }
25
+ elsif value == Date
26
+ { type: 'string', format: 'date' }
27
+ elsif [Time, DateTime].any?(&value.ancestors.method(:include?))
28
+ { type: 'string', format: 'date-time' }
29
+ end
30
+ end
31
+
32
+ def primitive_name(value)
33
+ return 'null' if value == NilClass
34
+ return 'integer' if value == Integer
35
+ return 'number' if value == Float
36
+ return 'string' if value == String
37
+ return 'boolean' if [TrueClass, FalseClass].include?(value)
38
+ end
39
+ end
40
+
41
+ class TooComplicatedError < ::SoberSwag::Compiler::Error; end
42
+ class TooComplicatedForPathError < TooComplicatedError; end
43
+ class TooComplicatedForQueryError < TooComplicatedError; end
44
+
45
+ def initialize(type)
46
+ @type = type
47
+ end
48
+
49
+ attr_reader :type
50
+
51
+ ##
52
+ # Is this type standalone, IE, worth serializing on its own
53
+ # in the schemas section of our schema?
54
+ def standalone?
55
+ type.is_a?(Class)
56
+ end
57
+
58
+ def object_schema
59
+ @object_schema ||=
60
+ normalize(parsed_type).cata(&method(:to_object_schema))
61
+ end
62
+
63
+ def schema_stub
64
+ @schema_stub ||= generate_schema_stub
65
+ end
66
+
67
+ def path_schema
68
+ path_schema_stub.map { |e| e.merge(in: :path) }
69
+ rescue TooComplicatedError => e
70
+ raise TooComplicatedForPathError, e.message
71
+ end
72
+
73
+ def query_schema
74
+ path_schema_stub.map { |e| e.merge(in: :query) }
75
+ rescue TooComplicatedError => e
76
+ raise TooComplicatedForQueryError, e.message
77
+ end
78
+
79
+ def ref_name
80
+ self.class.safe_name(type)
81
+ end
82
+
83
+ def found_types
84
+ @found_types ||=
85
+ begin
86
+ (_, found_types) = parsed_result
87
+ found_types
88
+ end
89
+ end
90
+
91
+ def parsed_type
92
+ @parsed_type ||=
93
+ begin
94
+ (parsed, _) = parsed_result
95
+ parsed
96
+ end
97
+ end
98
+
99
+ def parsed_result
100
+ @parsed_result ||= Parser.new(type_for_parser).run_parser
101
+ end
102
+
103
+ def eql?(other)
104
+ other.class == self.class && other.type == type
105
+ end
106
+
107
+ def hash
108
+ [self.class, type].hash
109
+ end
110
+
111
+ private
112
+
113
+ def generate_schema_stub
114
+ return self.class.primitive_def(type) if self.class.primitive?(type)
115
+
116
+ case type
117
+ when Class
118
+ { :$ref => self.class.get_ref(type) }
119
+ when Dry::Types::Constrained
120
+ self.class.new(type.type).schema_stub
121
+ when Dry::Types::Array::Member
122
+ { type: :array, items: self.class.new(type.member).schema_stub }
123
+ else
124
+ raise ArgumentError, "Cannot generate a schema stub for #{type} (#{type.class})"
125
+ end
126
+ end
127
+
128
+ def type_for_parser
129
+ if type.is_a?(Class)
130
+ type.schema.type
131
+ else
132
+ # Probably a constrained array
133
+ type
134
+ end
135
+ end
136
+
137
+ def normalize(object)
138
+ object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
139
+ end
140
+
141
+ def rewrite_sums(object)
142
+ case object
143
+ in Nodes::Sum[Nodes::OneOf[*lhs], Nodes::OneOf[*rhs]]
144
+ Nodes::OneOf.new(lhs + rhs)
145
+ in Nodes::Sum[Nodes::OneOf[*args], rhs]
146
+ Nodes::OneOf.new(args + [rhs])
147
+ in Nodes::Sum[lhs, Nodes::OneOf[*args]]
148
+ Nodes::OneOf.new([lhs] + args)
149
+ in Nodes::Sum[lhs, rhs]
150
+ Nodes::OneOf.new([lhs, rhs])
151
+ else
152
+ object
153
+ end
154
+ end
155
+
156
+ def flatten_one_ofs(object)
157
+ case object
158
+ in Nodes::OneOf[*args]
159
+ Nodes::OneOf.new(args.uniq)
160
+ else
161
+ object
162
+ end
163
+ end
164
+
165
+ def to_object_schema(object)
166
+ case object
167
+ in Nodes::List[element]
168
+ {
169
+ type: :array,
170
+ items: element
171
+ }
172
+ in Nodes::Enum[values]
173
+ {
174
+ type: :string,
175
+ enum: values
176
+ }
177
+ in Nodes::OneOf[{ type: 'null' }, b]
178
+ b.merge(nullable: true)
179
+ in Nodes::OneOf[a, { type: 'null' }]
180
+ a.merge(nullable: true)
181
+ in Nodes::OneOf[*attrs] if attrs.include?(type: 'null')
182
+ { oneOf: attrs.reject { |e| e[:type] == 'null' }, nullable: true }
183
+ in Nodes::OneOf[*cases]
184
+ { oneOf: cases }
185
+ in Nodes::Object[*attrs]
186
+ # openAPI requires that you give a list of required attributes
187
+ # (which IMO is the *totally* wrong thing to do but whatever)
188
+ # so we must do this garbage
189
+ required = attrs.filter {|(_, b)| b[:required] }.map(&:first)
190
+ {
191
+ type: :object,
192
+ properties: attrs.map { |(a,b)|
193
+ [a, b.select { |k, _| k != :required }]
194
+ }.to_h,
195
+ required: required
196
+ }
197
+ in Nodes::Attribute[name, true, value]
198
+ [name, value.merge(required: true)]
199
+ in Nodes::Attribute[name, false, value]
200
+ [name, value]
201
+ # can't match on value directly as ruby uses `===` to match,
202
+ # and classes use `===` to mean `is an instance of`, as
203
+ # opposed to direct equality lmao
204
+ in Nodes::Primitive[value:] if self.class.primitive?(value)
205
+ self.class.primitive_def(value)
206
+ in Nodes::Primitive[value:]
207
+ { '$ref': self.class.get_ref(value) }
208
+ else
209
+ raise ArgumentError, "Got confusing node #{object} (#{object.class})"
210
+ end
211
+ end
212
+
213
+ def path_schema_stub
214
+ @path_schema_stub ||=
215
+ object_schema[:properties].map do |k, v|
216
+ ensure_uncomplicated(k, v)
217
+ {
218
+ name: k,
219
+ schema: v.reject { |k, _| %i[required nullable].include?(k) },
220
+ allowEmptyValue: !object_schema[:required].include?(k) || !!v[:nullable], # if it's required, no empties, but if *nullabe*, empties are okay
221
+ required: object_schema[:required].include?(k) || false,
222
+ }
223
+ end
224
+ end
225
+
226
+ def ensure_uncomplicated(key, value)
227
+ return if value[:type]
228
+ raise TooComplicatedError, <<~ERROR
229
+ Property #{key} has object-schema #{value}, but this type of param should be simple (IE a primitive of some kind)
230
+ ERROR
231
+ end
232
+
233
+ end
234
+ end
235
+ end