odata_duty 0.30.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +312 -0
  3. data/lib/generators/odata_duty/entity_set/entity_set_generator.rb +56 -0
  4. data/lib/generators/odata_duty/entity_set/templates/entity_set.rb.erb +20 -0
  5. data/lib/generators/odata_duty/entity_set/templates/entity_set_spec.rb.erb +38 -0
  6. data/lib/generators/odata_duty/entity_set/templates/entity_type.rb.erb +6 -0
  7. data/lib/generators/odata_duty/entity_set/templates/entity_type_spec.rb.erb +20 -0
  8. data/lib/generators/odata_duty/entity_set/templates/odata_active_record_concern.rb.erb +109 -0
  9. data/lib/generators/odata_duty/entity_set/templates/resolver.rb.erb +39 -0
  10. data/lib/generators/odata_duty/entity_set/templates/resolver_spec.rb.erb +50 -0
  11. data/lib/generators/odata_duty/install/install_generator.rb +60 -0
  12. data/lib/generators/odata_duty/install/templates/controller.rb.tt +43 -0
  13. data/lib/generators/odata_duty/install/templates/schema.rb.tt +16 -0
  14. data/lib/metadata.xml.erb +116 -0
  15. data/lib/odata_duty/complex_type.rb +70 -0
  16. data/lib/odata_duty/context_wrapper.rb +26 -0
  17. data/lib/odata_duty/create_complex_type_hash_wrapper.rb +49 -0
  18. data/lib/odata_duty/dynamic_object_wrapper.rb.erb +79 -0
  19. data/lib/odata_duty/edms.rb +122 -0
  20. data/lib/odata_duty/edmx_schema.rb +19 -0
  21. data/lib/odata_duty/entity_type.rb +72 -0
  22. data/lib/odata_duty/enum_type.rb +71 -0
  23. data/lib/odata_duty/errors.rb +52 -0
  24. data/lib/odata_duty/executor.rb +277 -0
  25. data/lib/odata_duty/filter.rb +76 -0
  26. data/lib/odata_duty/filter_predicate.rb +12 -0
  27. data/lib/odata_duty/mapper_builder.rb +86 -0
  28. data/lib/odata_duty/mcp_input_schemas.rb +59 -0
  29. data/lib/odata_duty/mcp_server_builder.rb +107 -0
  30. data/lib/odata_duty/oas2/collection_get_path.rb +97 -0
  31. data/lib/odata_duty/oas2/collection_post_path.rb +63 -0
  32. data/lib/odata_duty/oas2/individual_delete_path.rb +42 -0
  33. data/lib/odata_duty/oas2/individual_get_path.rb +37 -0
  34. data/lib/odata_duty/oas2/individual_patch_path.rb +63 -0
  35. data/lib/odata_duty/oas2.rb +113 -0
  36. data/lib/odata_duty/parslet_search_expression.rb +163 -0
  37. data/lib/odata_duty/property/collection_prop.rb +25 -0
  38. data/lib/odata_duty/property/single_prop.rb +131 -0
  39. data/lib/odata_duty/property.rb +49 -0
  40. data/lib/odata_duty/railtie.rb +11 -0
  41. data/lib/odata_duty/schema_builder/complex_type.rb +34 -0
  42. data/lib/odata_duty/schema_builder/container.rb +16 -0
  43. data/lib/odata_duty/schema_builder/data_type.rb +18 -0
  44. data/lib/odata_duty/schema_builder/endpoint.rb +117 -0
  45. data/lib/odata_duty/schema_builder/entity_set.rb +67 -0
  46. data/lib/odata_duty/schema_builder/entity_type.rb +49 -0
  47. data/lib/odata_duty/schema_builder/enum_type.rb +32 -0
  48. data/lib/odata_duty/schema_builder.rb +130 -0
  49. data/lib/odata_duty/set_resolver.rb +51 -0
  50. data/lib/odata_duty.rb +311 -0
  51. metadata +168 -0
@@ -0,0 +1,71 @@
1
+ module OdataDuty
2
+ class EnumMember
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name.to_str
7
+ end
8
+ end
9
+
10
+ class EnumType
11
+ def self.members
12
+ @members ||= []
13
+ end
14
+
15
+ def self.member(name)
16
+ members << EnumMember.new(name)
17
+ end
18
+
19
+ class Metadata
20
+ attr_reader :enum_type
21
+
22
+ def initialize(enum_type)
23
+ @enum_type = enum_type
24
+ end
25
+
26
+ def members
27
+ enum_type.members
28
+ end
29
+
30
+ def scalar?
31
+ true
32
+ end
33
+
34
+ def name
35
+ enum_type.to_s.split('::').last.sub(/EnumType\z/, '').sub(/Enum\z/, '')
36
+ end
37
+
38
+ def metadata_type
39
+ :enum
40
+ end
41
+
42
+ def property_type
43
+ name
44
+ end
45
+ end
46
+
47
+ def self.__metadata
48
+ Metadata.new(self)
49
+ end
50
+
51
+ attr_reader :object
52
+
53
+ def initialize(object, _context)
54
+ @object = object
55
+ end
56
+
57
+ def self.to_value(*)
58
+ new(*).__to_value
59
+ end
60
+
61
+ def __to_value
62
+ return object if __member_names.include?(object)
63
+
64
+ raise InvalidValue, "#{object} is not a valid member of #{__member_names}"
65
+ end
66
+
67
+ def __member_names
68
+ @__member_names ||= self.class.members.map(&:name)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,52 @@
1
+ module OdataDuty
2
+ class Error < StandardError; end
3
+
4
+ class PropertyAlreadyDefinedError < ArgumentError; end
5
+ class InvalidNCNamesError < ArgumentError; end
6
+ class InitArgsMismatchError < ArgumentError; end
7
+
8
+ class RequestError < Error
9
+ attr_reader :code, :target
10
+
11
+ def initialize(message = 'Request Error', code: nil, target: nil)
12
+ super(message)
13
+ @code = code
14
+ @target = target
15
+ end
16
+
17
+ def status
18
+ :internal_server_error
19
+ end
20
+ end
21
+
22
+ class ServerError < RequestError; end
23
+
24
+ class NoImplementationError < ServerError
25
+ def status
26
+ :not_implemented
27
+ end
28
+ end
29
+
30
+ class NotYetSupportedError < NoImplementationError; end
31
+ class InvalidValue < ServerError; end
32
+
33
+ class ClientError < RequestError
34
+ def status
35
+ :bad_request
36
+ end
37
+ end
38
+
39
+ class ResourceNotFoundError < ClientError
40
+ def status
41
+ :not_found
42
+ end
43
+ end
44
+
45
+ class UnknownPropertyError < ClientError; end
46
+ class UnknownCollectionError < ClientError; end
47
+ class InvalidFilterValue < ClientError; end
48
+ class InvalidPropertyReferenceValue < ClientError; end
49
+ class InvalidType < ClientError; end
50
+ class NoSuchPropertyError < ClientError; end
51
+ class InvalidQueryOptionError < ClientError; end
52
+ end
@@ -0,0 +1,277 @@
1
+ require_relative 'context_wrapper'
2
+ require_relative 'parslet_search_expression'
3
+
4
+ module OdataDuty
5
+ class Executor # rubocop:disable Metrics/ClassLength
6
+ def self.execute(**)
7
+ new(**).execute
8
+ end
9
+
10
+ def self.create(**)
11
+ new(**).create
12
+ end
13
+
14
+ def self.update(**)
15
+ new(**).update
16
+ end
17
+
18
+ def self.delete(**)
19
+ new(**).delete
20
+ end
21
+
22
+ attr_reader :schema, :url, :context, :query_options
23
+
24
+ def initialize(schema:, url:, context:, query_options:)
25
+ @schema = schema
26
+ @url = url
27
+ @context = context
28
+ @query_options = query_options
29
+ end
30
+
31
+ def points = schema.endpoints
32
+
33
+ def execute
34
+ set_builder = prepare_builder(endpoint, wrapped_context, query_options)
35
+ select = query_options['$select']
36
+ props = selected(endpoint.entity_type, select) if select
37
+ apply_select(set_builder, props)
38
+
39
+ if url.include?('(')
40
+ individual(set_builder, endpoint, wrapped_context, props)
41
+ elsif url.include?('/$count')
42
+ apply_search(set_builder, query_options['$search'])
43
+ set_builder.count
44
+ else
45
+ collection(set_builder, endpoint, wrapped_context, query_options, props)
46
+ end
47
+ end
48
+
49
+ def create
50
+ unless endpoint.supports_create?
51
+ raise NoImplementationError, "create not implemented for #{endpoint.url}"
52
+ end
53
+
54
+ Oj.dump(endpoint
55
+ .create(context: wrapped_context)
56
+ .merge(
57
+ '@odata.context': wrapped_context.od_full_url('$metadata',
58
+ anchor: "#{endpoint.name}/$entity")
59
+ ),
60
+ mode: :compat)
61
+ end
62
+
63
+ def update
64
+ unless endpoint.supports_update?
65
+ raise NoImplementationError, "update not implemented for #{endpoint.url}"
66
+ end
67
+
68
+ entity_id = extract_value_from_brackets(url)
69
+ Oj.dump(endpoint
70
+ .update(entity_id, context: wrapped_context)
71
+ .merge(
72
+ '@odata.context': wrapped_context.od_full_url('$metadata',
73
+ anchor: "#{endpoint.name}/$entity")
74
+ ),
75
+ mode: :compat)
76
+ end
77
+
78
+ def delete
79
+ unless endpoint.supports_delete?
80
+ raise NoImplementationError, "delete not implemented for #{endpoint.url}"
81
+ end
82
+
83
+ entity_id = extract_value_from_brackets(url)
84
+ endpoint.delete(entity_id, context: wrapped_context)
85
+ Oj.dump({ '@odata.context': wrapped_context.od_full_url('$metadata') }, mode: :compat)
86
+ end
87
+
88
+ def prepare_builder(endpoint, context, query_options)
89
+ endpoint.new_entity_set(context: context).tap do |set_builder|
90
+ filter = query_options['$filter']
91
+ apply_filter(endpoint, set_builder, filter) if query_options.key?('$filter')
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def wrapped_context
98
+ @wrapped_context ||= ContextWrapper.new(context, base_url: schema.base_url,
99
+ endpoint: endpoint,
100
+ query_options: query_options)
101
+ end
102
+
103
+ def endpoint
104
+ @endpoint ||= points
105
+ .find { |e| url.split('/$count').first.split('(').first == e.url }
106
+ .tap do |result|
107
+ raise UnknownPropertyError, "No endpoint #{url} found in #{urls}" unless result
108
+ end
109
+ end
110
+
111
+ def urls = points.map(&:url)
112
+
113
+ def apply_filter(endpoint, set_builder, filter_string)
114
+ filters = Filter.parse(filter_string)
115
+ if Filter.or?(filter_string)
116
+ apply_or_filter(endpoint, set_builder, filters)
117
+ else
118
+ filters.each { |filter| apply_and_filter(endpoint, set_builder, filter) }
119
+ end
120
+ end
121
+
122
+ def apply_and_filter(endpoint, set_builder, filter)
123
+ value = filter_value(endpoint, set_builder, filter)
124
+ _filter(filter, set_builder, value)
125
+ end
126
+
127
+ def apply_or_filter(endpoint, set_builder, filters)
128
+ unless set_builder.respond_to?(:od_filter_or)
129
+ raise NoImplementationError, 'OR filtering not supported'
130
+ end
131
+
132
+ predicates = filters.map do |filter|
133
+ FilterPredicate.new(property_name: filter.property_name, operation: filter.operation,
134
+ value: filter_value(endpoint, set_builder, filter))
135
+ end
136
+ set_builder.od_filter_or(predicates)
137
+ end
138
+
139
+ def filter_value(endpoint, set_builder, filter)
140
+ property = endpoint.entity_type.properties.find { |p| p.name == filter.property_name }
141
+ assert_filter_valid_for_property(filter, property)
142
+ property.filter_convert(filter.value, set_builder.context)
143
+ end
144
+
145
+ def assert_filter_valid_for_property(filter, property)
146
+ raise UnknownPropertyError, "No such property #{filter.property_name}" unless property
147
+
148
+ return unless property.collection?
149
+
150
+ raise InvalidQueryOptionError,
151
+ "Cannot apply '#{filter.operation}' to a collection property '#{property.name}'."
152
+ end
153
+
154
+ def _filter(filter, set_builder, value)
155
+ specific_method = :"od_filter_#{filter.property_name}_#{filter.operation}"
156
+ generic_method = :"od_filter_#{filter.operation}"
157
+ if set_builder.respond_to?(specific_method)
158
+ set_builder.public_send(specific_method, value)
159
+ elsif set_builder.respond_to?(generic_method)
160
+ set_builder.public_send(generic_method, filter.property_name, value)
161
+ else
162
+ raise NoImplementationError, "#{filter.property_name} #{filter.operation} not supported"
163
+ end
164
+ end
165
+
166
+ def extract_value_from_brackets(url)
167
+ match_data = url.match(/.*\(([^)]+)\)/)
168
+ return nil unless match_data
169
+
170
+ value = match_data[1]
171
+
172
+ value.gsub(/\A"|"\z/, '').gsub(/\A'|'\z/, '')
173
+ end
174
+
175
+ def selected(entity_type, select_query)
176
+ valid_selected(select_query.split(',').map(&:strip)).map do |p|
177
+ entity_type.properties.find { |a| a.name.to_s == p }.tap do |prop|
178
+ raise UnknownPropertyError, "The property '#{p}' does not exist" unless prop
179
+ end
180
+ end
181
+ rescue UnknownPropertyError, InvalidQueryOptionError => e
182
+ e.backtrace.unshift entity_type._defined_at_
183
+ raise
184
+ end
185
+
186
+ def valid_selected(selected)
187
+ selected.each do |p|
188
+ unless Property.valid_name?(p)
189
+ raise InvalidQueryOptionError, "The property '#{p}' is not valid"
190
+ end
191
+ end
192
+ end
193
+
194
+ def apply_remaining(query_options, set_builder)
195
+ query_options.except('$count', '$filter', '$select')
196
+ .select { |k, _| k.start_with?('$') }.each do |k, v|
197
+ __send__("apply_#{k[1..]}", set_builder, v)
198
+ end
199
+ end
200
+
201
+ def apply_select(set_builder, props)
202
+ return unless set_builder.respond_to?(:od_select)
203
+
204
+ selected = (props || endpoint.entity_type.properties).map(&:name)
205
+ selected += endpoint.entity_type.property_refs.map(&:name)
206
+ set_builder.od_select(selected.uniq)
207
+ end
208
+
209
+ def apply_top(set_builder, top)
210
+ if !set_builder.respond_to?(:od_top) && top
211
+ raise NoImplementationError, "$top not implemented for #{set_builder.class}"
212
+ end
213
+
214
+ set_builder.od_top(top) if top
215
+ end
216
+
217
+ def apply_skip(set_builder, skip)
218
+ if !set_builder.respond_to?(:od_skip) && skip
219
+ raise NoImplementationError, "$skip not implemented for #{set_builder.class}"
220
+ end
221
+
222
+ set_builder.od_skip(skip) if skip
223
+ end
224
+
225
+ def apply_skiptoken(set_builder, skiptoken)
226
+ if !set_builder.respond_to?(:od_skiptoken) && skiptoken
227
+ raise NoImplementationError, "$skiptoken not implemented for #{set_builder.class}"
228
+ end
229
+
230
+ set_builder.od_skiptoken(skiptoken) if skiptoken
231
+ end
232
+
233
+ def apply_search(set_builder, search_string)
234
+ if !set_builder.respond_to?(:od_search) && search_string
235
+ raise NoImplementationError, "$search not implemented for #{set_builder.class}"
236
+ end
237
+
238
+ return unless search_string
239
+
240
+ search_expression = SearchExpression.parse(search_string)
241
+ set_builder.od_search(search_expression)
242
+ end
243
+
244
+ def add_next_link(data, set_builder, query_options, context)
245
+ return unless set_builder.od_next_link_skiptoken
246
+
247
+ next_query_options = query_options.merge(:$skiptoken => set_builder.od_next_link_skiptoken)
248
+ data[:'@odata.nextLink'] = context.od_full_url(url, **next_query_options)
249
+ end
250
+
251
+ require 'oj'
252
+
253
+ def collection(set_builder, endpoint, context, query_options, props)
254
+ count = set_builder.count if query_options['$count'] == 'true'
255
+ apply_remaining(query_options, set_builder)
256
+ data = { '@odata.context' => context.od_full_url('$metadata', anchor: endpoint.name),
257
+ 'value' => endpoint.collection(set_builder, context: context, selected: props) }
258
+ data['@odata.count'] = count if count
259
+ add_next_link(data, set_builder, query_options, context)
260
+ Oj.dump(data, mode: :compat)
261
+ end
262
+
263
+ def individual(set_builder, endpoint, context, props)
264
+ entity_id = extract_value_from_brackets(url)
265
+ apply_remaining(query_options, set_builder)
266
+
267
+ Oj.dump(
268
+ endpoint
269
+ .individual(set_builder, entity_id, context: context, selected: props)
270
+ .merge(
271
+ '@odata.context': context.od_full_url('$metadata', anchor: "#{endpoint.name}/$entity")
272
+ ),
273
+ mode: :compat
274
+ )
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,76 @@
1
+ module OdataDuty
2
+ class Filter
3
+ def self.parse(str)
4
+ validate(str)
5
+ separator = or?(str) ? ' or ' : ' and '
6
+ split_outside_quotes(str, separator).map { |s| new(s) }
7
+ end
8
+
9
+ def self.or?(str)
10
+ mask_quoted(str).include?(' or ')
11
+ end
12
+
13
+ def self.validate(str)
14
+ masked = mask_quoted(str)
15
+ if masked.include?(' and ') && masked.include?(' or ')
16
+ raise NotYetSupportedError, 'mixed AND/OR not supported'
17
+ end
18
+
19
+ %w[add sub mul div mod].each do |operator|
20
+ if masked.include?(" #{operator} ")
21
+ raise NotYetSupportedError,
22
+ 'filtering with arithmetic operators not supported'
23
+ end
24
+ end
25
+ return unless masked.include?('(')
26
+
27
+ raise NotYetSupportedError,
28
+ 'filtering does not support functions or Grouping Operators'
29
+ end
30
+
31
+ # Replace the contents of single-quoted literals with spaces so substring
32
+ # checks never match separators inside a value. Doubled quotes ('') escape.
33
+ def self.mask_quoted(str)
34
+ in_quote = false
35
+ str.each_char.map do |char|
36
+ if char == "'"
37
+ in_quote = !in_quote
38
+ char
39
+ else
40
+ in_quote ? ' ' : char
41
+ end
42
+ end.join
43
+ end
44
+
45
+ def self.split_outside_quotes(str, separator)
46
+ remaining = str.dup
47
+ mask_quoted(str).split(separator).map do |masked_segment|
48
+ segment = remaining.slice!(0, masked_segment.length)
49
+ remaining.slice!(0, separator.length)
50
+ segment
51
+ end
52
+ end
53
+
54
+ private_class_method :mask_quoted, :split_outside_quotes
55
+
56
+ def initialize(filter_string)
57
+ @property_name, @operation, @value = filter_string.split(nil, 3)
58
+ end
59
+
60
+ def value
61
+ @value.delete_prefix("'").delete_suffix("'")
62
+ end
63
+
64
+ def operation
65
+ @operation.to_sym
66
+ end
67
+
68
+ def property_name
69
+ if @property_name.include?('/')
70
+ raise NotYetSupportedError, 'nested property filtering not supported yet'
71
+ end
72
+
73
+ @property_name.to_sym
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ module OdataDuty
2
+ class FilterPredicate
3
+ attr_reader :property_name, :operation, :value
4
+
5
+ def initialize(property_name:, operation:, value:)
6
+ @property_name = property_name
7
+ @operation = operation
8
+ @value = value
9
+ freeze
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,86 @@
1
+ require 'securerandom'
2
+
3
+ module OdataDuty
4
+ class MapperBuilder
5
+ def self.build(complex_type, selected: nil, &)
6
+ mapper_class = build_class(complex_type, selected: selected)
7
+ mapper_class.new(complex_type, &)
8
+ end
9
+
10
+ def self.build_class(complex_type, selected:)
11
+ @dynamic_classes ||= {}
12
+ @dynamic_classes[complex_type] ||= {}
13
+ complex_classes = @dynamic_classes.fetch(complex_type)
14
+ selected_key = selected&.map(&:name)&.sort
15
+ complex_classes[selected_key] ||= eval_erb_class(complex_type, selected: selected)
16
+ end
17
+
18
+ require 'erb'
19
+ ERB_TEMPLATE = ERB.new(File.read("#{__dir__}/dynamic_object_wrapper.rb.erb"), trim_mode: '<>')
20
+ ERB_TEMPLATE.location = ["#{__dir__}/dynamic_object_wrapper.rb.erb", 1]
21
+ ERB_TEMPLATE.freeze
22
+
23
+ def self.eval_erb_class(complex_type, selected:)
24
+ properties = selected || complex_type.properties
25
+ class_result = ERB_TEMPLATE.result(binding)
26
+
27
+ eval(class_result) # rubocop:disable Security/Eval
28
+ end
29
+
30
+ attr_accessor :obj, :context
31
+ attr_reader :complex_type, :set_types, :calling_methods
32
+
33
+ # generated mappers fetch only the entries they need, so no filtering here
34
+ def initialize(complex_type, &block)
35
+ @complex_type = complex_type
36
+ @set_types = complex_type.properties.to_h { |cp| [cp.name, cp.set_type] }
37
+ @calling_methods = complex_type.properties.to_h { |cp| [cp.name, cp.calling_method] }
38
+ @block = block
39
+ end
40
+
41
+ def wrapped_obj
42
+ @wrapped_obj ||= complex_type.new(obj, context)
43
+ end
44
+
45
+ def obj_to_hash(obj, context)
46
+ @wrapped_obj = nil
47
+ self.context = context
48
+ self.obj = obj
49
+ base_hash = to_h
50
+ @block.call(base_hash, obj)
51
+ base_hash
52
+ end
53
+
54
+ def obj_to_base_hash(obj)
55
+ return nil if obj.nil?
56
+
57
+ @wrapped_obj = nil
58
+ self.obj = obj
59
+ to_h
60
+ end
61
+
62
+ VALID_BOOLEAN_VALUES = [true, false, nil].freeze
63
+
64
+ def confirm_boolean(name, value)
65
+ case value
66
+ when true, false, nil
67
+ value
68
+ else
69
+ raise InvalidValue, "Property #{name} must be a boolean and not #{value}"
70
+ end
71
+ end
72
+
73
+ def not_nullable(name, value)
74
+ raise InvalidValue, "Property #{name} cannot be null" if value.nil?
75
+
76
+ value
77
+ end
78
+
79
+ def confirm_one_of(name, value, valid_values)
80
+ return nil if value.nil?
81
+ return value if valid_values.include?(value)
82
+
83
+ raise InvalidValue, "Property #{name} must be one of #{valid_values} and not #{value}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,59 @@
1
+ module OdataDuty
2
+ module McpInputSchemas
3
+ extend self
4
+
5
+ # Property names are used as-is (symbols) for keys/required and the root `type: object` is
6
+ # omitted; the MCP SDK normalizes the keys and supplies the root type default.
7
+
8
+ def count_input_schema(supports_search:)
9
+ properties = { '$filter' => { 'type' => 'string' } }
10
+ properties['$search'] = { 'type' => 'string' } if supports_search
11
+ { 'properties' => properties, 'required' => [] }
12
+ end
13
+
14
+ def list_input_schema(supports_search:)
15
+ properties = {
16
+ '$filter' => { 'type' => 'string', 'description' => 'OData $filter expression' },
17
+ '$select' => { 'type' => 'string',
18
+ 'description' => 'Comma-separated properties to return' }
19
+ }
20
+ if supports_search
21
+ properties['$search'] = { 'type' => 'string',
22
+ 'description' => 'Search expression (AND, OR, NOT)' }
23
+ end
24
+ properties['$top'] = { 'type' => 'integer', 'description' => 'Max records to return' }
25
+ properties['$skip'] = { 'type' => 'integer', 'description' => 'Records to skip' }
26
+ { 'properties' => properties, 'required' => [] }
27
+ end
28
+
29
+ def create_input_schema(entity_type)
30
+ writable = entity_type.properties.select(&:settable_on_create?)
31
+ properties = writable.to_h { |p| [p.name, p.to_oas2] }
32
+ required = writable.reject(&:nullable).map(&:name)
33
+ { 'properties' => properties, 'required' => required }
34
+ end
35
+
36
+ def update_input_schema(entity_type)
37
+ key = entity_type.property_refs.first
38
+ writable = entity_type.properties.select(&:settable_on_update?)
39
+ properties = { key.name => key.to_oas2 }
40
+ writable.each { |p| properties[p.name] = p.to_oas2 }
41
+ { 'properties' => properties, 'required' => [key.name] }
42
+ end
43
+
44
+ def get_input_schema(entity_type)
45
+ key = entity_type.property_refs.first
46
+ properties = {
47
+ key.name => key.to_oas2,
48
+ '$select' => { 'type' => 'string',
49
+ 'description' => 'Comma-separated properties to return' }
50
+ }
51
+ { 'properties' => properties, 'required' => [key.name] }
52
+ end
53
+
54
+ def delete_input_schema(entity_type)
55
+ key = entity_type.property_refs.first
56
+ { 'properties' => { key.name => key.to_oas2 }, 'required' => [key.name] }
57
+ end
58
+ end
59
+ end