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