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,51 @@
1
+ module OdataDuty
2
+ class SetResolver
3
+ attr_reader :context
4
+
5
+ def initialize(context:, init_args:)
6
+ @context = context
7
+ return unless respond_to?(:od_after_init)
8
+
9
+ begin
10
+ call_od_after_init(init_args)
11
+ rescue InitArgsMismatchError => e
12
+ e.backtrace.insert(1, entity_set._defined_at_)
13
+ raise
14
+ rescue StandardError => e
15
+ e.backtrace.insert(2, entity_set._defined_at_)
16
+ raise
17
+ end
18
+ end
19
+
20
+ def od_next_link_skiptoken(token = nil)
21
+ @od_next_link_skiptoken = token.to_s if token
22
+ @od_next_link_skiptoken
23
+ end
24
+
25
+ private
26
+
27
+ def call_od_after_init(init_args)
28
+ case init_args
29
+ when nil then od_after_init
30
+ when Hash then od_after_init(**init_args)
31
+ when Array then od_after_init(*init_args)
32
+ else od_after_init(init_args)
33
+ end
34
+ rescue ArgumentError => e
35
+ handle_init_args_error(e)
36
+ end
37
+
38
+ def handle_init_args_error(arg_error)
39
+ raise unless arg_error.backtrace.first.include?("od_after_init'")
40
+
41
+ err = InitArgsMismatchError.new(arg_error)
42
+ err.set_backtrace(arg_error.backtrace)
43
+
44
+ raise err
45
+ end
46
+
47
+ def entity_set
48
+ context.endpoint.entity_set
49
+ end
50
+ end
51
+ end
data/lib/odata_duty.rb ADDED
@@ -0,0 +1,311 @@
1
+ require 'odata_duty/errors'
2
+ require 'odata_duty/set_resolver'
3
+ require 'odata_duty/schema_builder'
4
+ require 'odata_duty/edmx_schema'
5
+ require 'odata_duty/executor'
6
+ require 'odata_duty/mcp_server_builder'
7
+ require 'odata_duty/oas2'
8
+ require 'odata_duty/property'
9
+ require 'odata_duty/enum_type'
10
+ require 'odata_duty/complex_type'
11
+ require 'odata_duty/entity_type'
12
+ require 'odata_duty/filter'
13
+ require 'odata_duty/filter_predicate'
14
+ require 'odata_duty/create_complex_type_hash_wrapper'
15
+ # :nocov: Rails is not loaded when this file is required in the test suite
16
+ require 'odata_duty/railtie' if defined?(Rails::Railtie)
17
+ # :nocov:
18
+
19
+ module OdataDuty
20
+ class EntitySet
21
+ def self.entity_type(entity_type = nil)
22
+ @entity_type = entity_type if entity_type
23
+ @entity_type
24
+ end
25
+
26
+ def self.name(name = nil)
27
+ @name = name.to_s if name.is_a?(Symbol)
28
+ @name = name.to_str if name && !name.is_a?(Symbol)
29
+ @name
30
+ end
31
+
32
+ def self.url(url = nil)
33
+ @url = url.to_s if url.is_a?(Symbol)
34
+ @url = url.to_str if url && !url.is_a?(Symbol)
35
+ @url
36
+ end
37
+
38
+ class Metadata
39
+ attr_reader :entity_set
40
+
41
+ def initialize(entity_set)
42
+ @entity_set = entity_set
43
+ end
44
+
45
+ def metadata_types
46
+ [entity_type].map(&:__metadata).flat_map(&:metadata_types)
47
+ end
48
+
49
+ def name
50
+ entity_set.name ||
51
+ entity_set.to_s.split('::').last.sub(/EntitySet\z/, '').sub(/Set\z/, '')
52
+ end
53
+
54
+ def entity_type
55
+ entity_set.entity_type
56
+ end
57
+
58
+ def entity_type_name
59
+ entity_type.__metadata.name
60
+ end
61
+
62
+ def kind
63
+ 'EntitySet'
64
+ end
65
+
66
+ def url
67
+ entity_set.url || name
68
+ end
69
+
70
+ def new_entity_set(context:)
71
+ entity_set.new(context: context)
72
+ end
73
+
74
+ def collection(set_builder, context:, selected:)
75
+ begin
76
+ values = set_builder.collection
77
+ rescue NoMethodError
78
+ raise NoImplementationError, "collection not implemented for #{entity_set}"
79
+ end
80
+
81
+ mapper = entity_type.mapper(context, selected: selected)
82
+
83
+ values.map { |v| mapper.obj_to_hash(v, context) }
84
+ end
85
+
86
+ def individual(set_builder, id, context:, selected:)
87
+ begin
88
+ result = set_builder.individual(converted_id(id))
89
+ rescue NoMethodError
90
+ raise NoImplementationError, "individual not implemented for #{entity_set}"
91
+ end
92
+
93
+ raise ResourceNotFoundError, "No such entity #{id}" unless result
94
+
95
+ entity_type.mapper(context, selected: selected).obj_to_hash(result, context)
96
+ end
97
+
98
+ def create(context:)
99
+ wrapper = CreateComplexTypeHashWrapper.new(context.query_options, entity_type,
100
+ operation: :create, context: context)
101
+ result = entity_set.new(context: context)
102
+ .create(wrapper)
103
+ mapper = entity_type.mapper(context, selected: nil)
104
+ mapper.obj_to_hash(result, context)
105
+ end
106
+
107
+ def update(id, context:)
108
+ wrapper = CreateComplexTypeHashWrapper.new(context.query_options, entity_type,
109
+ operation: :update, context: context)
110
+ result = entity_set.new(context: context).update(converted_id(id), wrapper)
111
+ raise ResourceNotFoundError, "No such entity #{id}" unless result
112
+
113
+ entity_type.mapper(context, selected: nil).obj_to_hash(result, context)
114
+ end
115
+
116
+ def delete(id, context:)
117
+ result = entity_set.new(context: context).delete(converted_id(id))
118
+ raise ResourceNotFoundError, "No such entity #{id}" unless result
119
+ end
120
+
121
+ def supports_search?
122
+ # Check if the entity set class supports search by looking for the od_search method
123
+ entity_set.method_defined?(:od_search)
124
+ end
125
+
126
+ def supports_filter_or?
127
+ entity_set.method_defined?(:od_filter_or)
128
+ end
129
+
130
+ def supports_collection?
131
+ # Check if the entity set class supports read by looking for the collection method
132
+ entity_set.method_defined?(:collection)
133
+ end
134
+
135
+ def supports_individual?
136
+ # Check if the entity set class supports read-by-id via the individual method
137
+ entity_set.method_defined?(:individual)
138
+ end
139
+
140
+ def supports_count?
141
+ # Check if the entity set class supports counting via the count method
142
+ entity_set.method_defined?(:count)
143
+ end
144
+
145
+ def supports_create?
146
+ # Check if the entity set class supports create by looking for the create method
147
+ entity_set.method_defined?(:create)
148
+ end
149
+
150
+ def supports_update?
151
+ # Check if the entity set class supports update by looking for the update method
152
+ entity_set.method_defined?(:update)
153
+ end
154
+
155
+ def supports_delete?
156
+ # Check if the entity set class supports delete by looking for the delete method
157
+ entity_set.method_defined?(:delete)
158
+ end
159
+
160
+ def non_insertable_property_names
161
+ @non_insertable_property_names ||=
162
+ entity_type.properties.select(&:non_insertable?).map(&:name)
163
+ end
164
+
165
+ private
166
+
167
+ def converted_id(id)
168
+ entity_type.property_refs.first.convert(id, nil)
169
+ rescue OdataDuty::InvalidValue => e
170
+ raise InvalidPropertyReferenceValue, "Invalid individual id : #{e}"
171
+ end
172
+ end
173
+
174
+ def self.__metadata
175
+ Metadata.new(self)
176
+ end
177
+
178
+ attr_reader :context
179
+
180
+ def initialize(context:)
181
+ @context = context
182
+ od_after_init
183
+ end
184
+
185
+ def od_after_init; end
186
+
187
+ def od_next_link_skiptoken(token = nil)
188
+ @od_next_link_skiptoken = token.to_s if token
189
+ @od_next_link_skiptoken
190
+ end
191
+ end
192
+
193
+ class Schema
194
+ def self.namespace(name = nil)
195
+ @namespace = name if name
196
+ @namespace
197
+ end
198
+
199
+ def self.version(name = nil)
200
+ @version = name if name
201
+ @version
202
+ end
203
+
204
+ def self.title(name = nil)
205
+ @title = name if name
206
+ @title
207
+ end
208
+
209
+ def self.entity_sets(entity_sets = nil)
210
+ @entity_sets = entity_sets.uniq if entity_sets
211
+ @entity_sets
212
+ end
213
+
214
+ def self.base_url(url = nil)
215
+ @base_url = url if url
216
+ @base_url
217
+ end
218
+
219
+ class Metadata
220
+ attr_reader :schema
221
+
222
+ def initialize(schema)
223
+ @schema = schema
224
+ end
225
+
226
+ def version = schema.version
227
+ def title = schema.title
228
+
229
+ def namespace
230
+ schema.namespace
231
+ end
232
+
233
+ def entity_sets
234
+ schema.entity_sets.map(&:__metadata)
235
+ end
236
+
237
+ def metadata_types
238
+ @metadata_types ||= entity_sets.flat_map(&:metadata_types).uniq.map(&:__metadata)
239
+ end
240
+
241
+ def entity_types
242
+ metadata_types.select { |mt| mt.metadata_type == :entity }
243
+ end
244
+
245
+ def complex_types
246
+ metadata_types.select { |mt| mt.metadata_type == :complex }
247
+ end
248
+
249
+ def enum_types
250
+ metadata_types.select { |mt| mt.metadata_type == :enum }
251
+ end
252
+
253
+ def endpoints
254
+ entity_sets
255
+ end
256
+
257
+ def check_names
258
+ names = Set.new
259
+ (enum_types + complex_types + entity_types).each do |et|
260
+ raise "Duplicate #{et.name} type" if names.include?(et.name)
261
+
262
+ names << et.name
263
+ end
264
+ end
265
+ end
266
+
267
+ def self.__metadata
268
+ Metadata.new(self)
269
+ end
270
+
271
+ def self.metadata_xml
272
+ metadata = __metadata
273
+ metadata.check_names
274
+
275
+ ERB.new(File.read("#{__dir__}/metadata.xml.erb")).result(binding)
276
+ end
277
+
278
+ def self.index_hash(metadata_url)
279
+ {
280
+ '@odata.context': metadata_url,
281
+ value: endpoints.map do |e|
282
+ { name: e.name, kind: e.kind, url: e.url }
283
+ end
284
+ }
285
+ end
286
+
287
+ def self.endpoints
288
+ __metadata.endpoints
289
+ end
290
+
291
+ def self.execute(url, context:, query_options: {})
292
+ Executor.execute(url: url, context: context, query_options: query_options, schema: self)
293
+ end
294
+
295
+ def self.create(url, context:, query_options: nil)
296
+ Executor.create(url: url, context: context, query_options: query_options, schema: self)
297
+ end
298
+
299
+ def self.update(url, context:, query_options: nil)
300
+ Executor.update(url: url, context: context, query_options: query_options, schema: self)
301
+ end
302
+
303
+ def self.delete(url, context:, query_options: nil)
304
+ Executor.delete(url: url, context: context, query_options: query_options, schema: self)
305
+ end
306
+
307
+ def self.to_mcp_server
308
+ McpServerBuilder.build(self)
309
+ end
310
+ end
311
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: odata_duty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.30.0
5
+ platform: ruby
6
+ authors:
7
+ - Grant Petersen-Speelman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mcp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.20.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.20.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: oj
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.0.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 5.0.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.0.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 5.0.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: parslet
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: uri
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 1.0.0
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: 2.0.0
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 1.0.0
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: 2.0.0
87
+ description: A unified DSL for exposing structured data and operations across analytics
88
+ tools (PowerBI), no-code platforms (PowerAutomate), and AI agents (via MCP).
89
+ email:
90
+ - grant@nexl.io
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - README.md
96
+ - lib/generators/odata_duty/entity_set/entity_set_generator.rb
97
+ - lib/generators/odata_duty/entity_set/templates/entity_set.rb.erb
98
+ - lib/generators/odata_duty/entity_set/templates/entity_set_spec.rb.erb
99
+ - lib/generators/odata_duty/entity_set/templates/entity_type.rb.erb
100
+ - lib/generators/odata_duty/entity_set/templates/entity_type_spec.rb.erb
101
+ - lib/generators/odata_duty/entity_set/templates/odata_active_record_concern.rb.erb
102
+ - lib/generators/odata_duty/entity_set/templates/resolver.rb.erb
103
+ - lib/generators/odata_duty/entity_set/templates/resolver_spec.rb.erb
104
+ - lib/generators/odata_duty/install/install_generator.rb
105
+ - lib/generators/odata_duty/install/templates/controller.rb.tt
106
+ - lib/generators/odata_duty/install/templates/schema.rb.tt
107
+ - lib/metadata.xml.erb
108
+ - lib/odata_duty.rb
109
+ - lib/odata_duty/complex_type.rb
110
+ - lib/odata_duty/context_wrapper.rb
111
+ - lib/odata_duty/create_complex_type_hash_wrapper.rb
112
+ - lib/odata_duty/dynamic_object_wrapper.rb.erb
113
+ - lib/odata_duty/edms.rb
114
+ - lib/odata_duty/edmx_schema.rb
115
+ - lib/odata_duty/entity_type.rb
116
+ - lib/odata_duty/enum_type.rb
117
+ - lib/odata_duty/errors.rb
118
+ - lib/odata_duty/executor.rb
119
+ - lib/odata_duty/filter.rb
120
+ - lib/odata_duty/filter_predicate.rb
121
+ - lib/odata_duty/mapper_builder.rb
122
+ - lib/odata_duty/mcp_input_schemas.rb
123
+ - lib/odata_duty/mcp_server_builder.rb
124
+ - lib/odata_duty/oas2.rb
125
+ - lib/odata_duty/oas2/collection_get_path.rb
126
+ - lib/odata_duty/oas2/collection_post_path.rb
127
+ - lib/odata_duty/oas2/individual_delete_path.rb
128
+ - lib/odata_duty/oas2/individual_get_path.rb
129
+ - lib/odata_duty/oas2/individual_patch_path.rb
130
+ - lib/odata_duty/parslet_search_expression.rb
131
+ - lib/odata_duty/property.rb
132
+ - lib/odata_duty/property/collection_prop.rb
133
+ - lib/odata_duty/property/single_prop.rb
134
+ - lib/odata_duty/railtie.rb
135
+ - lib/odata_duty/schema_builder.rb
136
+ - lib/odata_duty/schema_builder/complex_type.rb
137
+ - lib/odata_duty/schema_builder/container.rb
138
+ - lib/odata_duty/schema_builder/data_type.rb
139
+ - lib/odata_duty/schema_builder/endpoint.rb
140
+ - lib/odata_duty/schema_builder/entity_set.rb
141
+ - lib/odata_duty/schema_builder/entity_type.rb
142
+ - lib/odata_duty/schema_builder/enum_type.rb
143
+ - lib/odata_duty/set_resolver.rb
144
+ homepage:
145
+ licenses: []
146
+ metadata:
147
+ rubygems_mfa_required: 'true'
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: 3.2.0
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubygems_version: 3.5.22
164
+ signing_key:
165
+ specification_version: 4
166
+ summary: A unified, intent-driven interface layer that serves automation, AI, and
167
+ developer platforms with minimal duplication.
168
+ test_files: []