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,60 @@
1
+ module OdataDuty
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ class_option :module, type: :string, default: nil, desc: 'Module namespace'
7
+
8
+ def create_controller
9
+ template 'controller.rb.tt',
10
+ File.join('app/controllers', controller_path, 'api_controller.rb')
11
+ end
12
+
13
+ def create_schema
14
+ template 'schema.rb.tt', File.join('app/schemas', controller_path, 'schema.rb')
15
+ end
16
+
17
+ def add_routes
18
+ route route_contents
19
+ end
20
+
21
+ private
22
+
23
+ def controller_path
24
+ options[:module]&.underscore || ''
25
+ end
26
+
27
+ def module_name
28
+ options[:module]&.camelize
29
+ end
30
+
31
+ def controller_class
32
+ [module_name, 'ApiController'].compact.join('::')
33
+ end
34
+
35
+ def schema_class
36
+ [module_name, 'Schema'].compact.join('::')
37
+ end
38
+
39
+ def route_controller
40
+ parts = []
41
+ parts << controller_path unless controller_path.empty?
42
+ parts << 'api'
43
+ parts.join('/')
44
+ end
45
+
46
+ def route_contents
47
+ <<~RUBY
48
+ scope '/api' do
49
+ root '#{route_controller}#index'
50
+ get '$metadata' => '#{route_controller}#metadata'
51
+ get '$oas2' => '#{route_controller}#oas2'
52
+ get '*url' => '#{route_controller}#show'
53
+ post '*url' => '#{route_controller}#create'
54
+ delete '*url' => '#{route_controller}#destroy'
55
+ end
56
+ RUBY
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,43 @@
1
+ class <%= controller_class %> < ApplicationController
2
+ def index
3
+ render json: OdataDuty::EdmxSchema.index_hash(schema_metadata_url)
4
+ end
5
+
6
+ def metadata
7
+ render xml: OdataDuty::EdmxSchema.metadata_xml(schema)
8
+ end
9
+
10
+ def oas2
11
+ # Power Automate's "Import from URL" is a browser-side cross-origin fetch; without this CORS
12
+ # header it fails with a misleading "couldn't download" error even though the server 200s.
13
+ response.set_header('Access-Control-Allow-Origin', '*')
14
+ render json: OdataDuty::OAS2.build_json(schema)
15
+ end
16
+
17
+ def show
18
+ render json: schema.execute(params[:url], context: self, query_options: query_options)
19
+ end
20
+
21
+ def create
22
+ render json: schema.create(params[:url], context: self, query_options: query_options)
23
+ end
24
+
25
+ def destroy
26
+ schema.delete(params[:url], context: self, query_options: query_options)
27
+ head :no_content
28
+ end
29
+
30
+ private
31
+
32
+ def query_options
33
+ params.to_unsafe_hash.except('url', 'action', 'controller', 'format')
34
+ end
35
+
36
+ def schema
37
+ <%= schema_class %>.build(request)
38
+ end
39
+
40
+ def schema_metadata_url
41
+ url_for(action: :metadata, only_path: false)
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ <% if module_name %>module <%= module_name %>
2
+ <% end %>class Schema
3
+ def self.build(request)
4
+ OdataDuty::SchemaBuilder.build(namespace: "<%= module_name || 'MySpace' %>",
5
+ host: request.host_with_port,
6
+ scheme: request.scheme,
7
+ base_path: '/api') do |s|
8
+ s.title = 'My Dynamic API'
9
+ s.version = '0.0.1'
10
+ # Add entity types and sets here
11
+ end
12
+ end
13
+ end
14
+ <% if module_name %>
15
+ end
16
+ <% end %>
@@ -0,0 +1,116 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" edmx:schemaLocation="https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/os/schemas/edm.xsd https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/os/schemas/edmx.xsd">
3
+ <edmx:Reference Uri="https://docs.oasis-open.org/odata/odata-vocabularies/v4.0/vocabularies/Org.OData.Capabilities.V1.xml">
4
+ <edmx:Include Namespace="Org.OData.Capabilities.V1" Alias="Capabilities" />
5
+ </edmx:Reference>
6
+ <edmx:Reference Uri="https://docs.oasis-open.org/odata/odata-vocabularies/v4.0/vocabularies/Org.OData.Core.V1.xml">
7
+ <edmx:Include Namespace="Org.OData.Core.V1" Alias="Core" />
8
+ </edmx:Reference>
9
+ <edmx:DataServices>
10
+ <Schema Namespace="<%= metadata.namespace %>" xmlns="http://docs.oasis-open.org/odata/ns/edm">
11
+ <% if metadata.version %><Annotation Term="<%= metadata.namespace %>.Version" String="<%= metadata.version %>" /><% end %>
12
+ <% if metadata.title %><Annotation Term="<%= metadata.namespace %>.Title" String="<%= metadata.title %>" /><% end %>
13
+
14
+ <% metadata.enum_types.each do |enum_type| %>
15
+ <EnumType Name="<%= enum_type.name %>">
16
+ <% enum_type.members.each do |member| %>
17
+ <Member Name="<%= member.name %>" />
18
+ <% end %>
19
+ </EnumType>
20
+ <% end %>
21
+ <% metadata.complex_types.each do |complex_type| %>
22
+ <ComplexType Name="<%= complex_type.name %>">
23
+ <% complex_type.properties.each do |property| %>
24
+ <% if property.core_annotation_term %>
25
+ <Property Name="<%= property.name %>" Nullable="<%= property.nullable %>" Type="<%= property.collection? ? "Collection(" : "" %><%= property.type.include?(".") ? "" : "#{metadata.namespace}." %><%= property.type %><%= property.collection? ? ")" : "" %>">
26
+ <Annotation Term="<%= property.core_annotation_term %>" Bool="true" />
27
+ </Property>
28
+ <% else %>
29
+ <Property Name="<%= property.name %>" Nullable="<%= property.nullable %>" Type="<%= property.collection? ? "Collection(" : "" %><%= property.type.include?(".") ? "" : "#{metadata.namespace}." %><%= property.type %><%= property.collection? ? ")" : "" %>" />
30
+ <% end %>
31
+ <% end %>
32
+ </ComplexType>
33
+ <% end %>
34
+ <% metadata.entity_types.each do |entity_type| %>
35
+ <EntityType Name="<%= entity_type.name %>">
36
+ <Key>
37
+ <% entity_type.property_refs.each do |property| %>
38
+ <PropertyRef Name="<%= property.name %>" />
39
+ <% end %>
40
+ </Key>
41
+ <% entity_type.properties.each do |property| %>
42
+ <% if property.core_annotation_term %>
43
+ <Property Name="<%= property.name %>" Nullable="<%= property.nullable %>" Type="<%= property.collection? ? "Collection(" : "" %><%= property.type.include?(".") ? "" : "#{metadata.namespace}." %><%= property.type %><%= property.collection? ? ")" : "" %>">
44
+ <Annotation Term="<%= property.core_annotation_term %>" Bool="true" />
45
+ </Property>
46
+ <% else %>
47
+ <Property Name="<%= property.name %>" Nullable="<%= property.nullable %>" Type="<%= property.collection? ? "Collection(" : "" %><%= property.type.include?(".") ? "" : "#{metadata.namespace}." %><%= property.type %><%= property.collection? ? ")" : "" %>" />
48
+ <% end %>
49
+ <% end %>
50
+ </EntityType>
51
+ <% end %>
52
+ <EntityContainer Name="Container">
53
+ <% metadata.entity_sets.each do |entity_set| %>
54
+ <EntitySet Name="<%= entity_set.name %>" EntityType="<%= metadata.namespace %>.<%= entity_set.entity_type_name %>">
55
+ <% if entity_set.supports_search? %>
56
+ <Annotation Term="Capabilities.SearchRestrictions">
57
+ <Record>
58
+ <PropertyValue Property="Searchable" Bool="true" />
59
+ <PropertyValue Property="UnsupportedExpressions" EnumMember="Capabilities.SearchExpressions/group" />
60
+ </Record>
61
+ </Annotation>
62
+ <% end %>
63
+ <% if entity_set.supports_filter_or? %>
64
+ <Annotation Term="Capabilities.FilterRestrictions">
65
+ <Record>
66
+ <PropertyValue Property="Filterable" Bool="true" />
67
+ <PropertyValue Property="FilterExpressionRestrictions">
68
+ <Collection>
69
+ <Record>
70
+ <PropertyValue Property="Property" PropertyPath="*" />
71
+ <PropertyValue Property="AllowedExpressions" EnumMember="Capabilities.FilterExpressionType/SingleValue" />
72
+ </Record>
73
+ </Collection>
74
+ </PropertyValue>
75
+ </Record>
76
+ </Annotation>
77
+ <% end %>
78
+ <% non_insertable = entity_set.non_insertable_property_names %>
79
+ <% if !entity_set.supports_create? || non_insertable.any? %>
80
+ <Annotation Term="Capabilities.InsertRestrictions">
81
+ <Record>
82
+ <% if !entity_set.supports_create? %>
83
+ <PropertyValue Property="Insertable" Bool="false" />
84
+ <% end %>
85
+ <% if non_insertable.any? %>
86
+ <PropertyValue Property="NonInsertableProperties">
87
+ <Collection>
88
+ <% non_insertable.each do |property_name| %>
89
+ <PropertyPath><%= property_name %></PropertyPath>
90
+ <% end %>
91
+ </Collection>
92
+ </PropertyValue>
93
+ <% end %>
94
+ </Record>
95
+ </Annotation>
96
+ <% end %>
97
+ <% if !entity_set.supports_update? %>
98
+ <Annotation Term="Capabilities.UpdateRestrictions">
99
+ <Record>
100
+ <PropertyValue Property="Updatable" Bool="false" />
101
+ </Record>
102
+ </Annotation>
103
+ <% end %>
104
+ <% if !entity_set.supports_delete? %>
105
+ <Annotation Term="Capabilities.DeleteRestrictions">
106
+ <Record>
107
+ <PropertyValue Property="Deletable" Bool="false" />
108
+ </Record>
109
+ </Annotation>
110
+ <% end %>
111
+ </EntitySet>
112
+ <% end %>
113
+ </EntityContainer>
114
+ </Schema>
115
+ </edmx:DataServices>
116
+ </edmx:Edmx>
@@ -0,0 +1,70 @@
1
+ require_relative 'mapper_builder'
2
+
3
+ module OdataDuty
4
+ class ComplexType
5
+ def self.properties
6
+ @properties ||= []
7
+ end
8
+
9
+ def self.property(name, *args, **kwargs)
10
+ if properties.any? { |p| p.name == name.to_sym }
11
+ raise PropertyAlreadyDefinedError, "#{name} is already defined"
12
+ end
13
+
14
+ Property.new(name, *args, **kwargs).tap do |property|
15
+ properties << property
16
+ end
17
+ end
18
+
19
+ class Metadata
20
+ attr_reader :complex_type
21
+
22
+ def initialize(complex_type)
23
+ @complex_type = complex_type
24
+ end
25
+
26
+ def properties
27
+ complex_type.properties
28
+ end
29
+
30
+ def name
31
+ complex_type.to_s.split('::').last.sub(/ComplexType\z/, '').sub(/Complex\z/, '')
32
+ end
33
+
34
+ def property_type
35
+ name
36
+ end
37
+
38
+ def metadata_type
39
+ :complex
40
+ end
41
+
42
+ # callers uniq the result, so duplicates from repeated property types are fine
43
+ def metadata_types(visited = [])
44
+ return [] if visited.include?(complex_type)
45
+
46
+ visited << complex_type
47
+ raw_types = properties.map(&:raw_type)
48
+ nested = raw_types.grep(Metadata).flat_map { |m| m.metadata_types(visited) }
49
+ [complex_type] +
50
+ nested +
51
+ raw_types.grep(EnumType::Metadata).map(&:enum_type)
52
+ end
53
+
54
+ # nil (falsy) rather than false: callers only consume truthiness
55
+ def scalar?; end
56
+ end
57
+
58
+ def self.__metadata
59
+ Metadata.new(self)
60
+ end
61
+
62
+ attr_reader :object
63
+ attr_reader :od_context
64
+
65
+ def initialize(object, context)
66
+ @object = object
67
+ @od_context = context
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,26 @@
1
+ require 'delegate'
2
+ require 'uri'
3
+
4
+ module OdataDuty
5
+ class ContextWrapper < SimpleDelegator
6
+ attr_reader :caller_context, :base_url, :endpoint, :query_options
7
+
8
+ def initialize(caller_context, base_url:, endpoint:, query_options:)
9
+ super(caller_context)
10
+ @base_url = base_url.chomp('/')
11
+ @endpoint = endpoint
12
+ @query_options = query_options.to_h
13
+ end
14
+
15
+ def od_full_url(path, anchor: nil, **query_params)
16
+ URI.parse([base_url, path].join('/')).tap do |uri|
17
+ uri.query = URI.encode_www_form(query_params) if query_params.any?
18
+ uri.fragment = anchor
19
+ end.to_str
20
+ end
21
+
22
+ def current
23
+ @current ||= {}
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ module OdataDuty
2
+ class CreateComplexTypeHashWrapper
3
+ SETTABLE_BY_OPERATION = { create: :settable_on_create?, update: :settable_on_update? }.freeze
4
+
5
+ def initialize(hash, complex_type, operation:, context:)
6
+ @hash = hash
7
+ @complex_type = complex_type
8
+ @operation = operation
9
+ @context = context
10
+ end
11
+
12
+ def method_missing(method_name, *args)
13
+ matching_prop = @complex_type.properties.find { |p| p.name == method_name }
14
+ unless matching_prop && args.empty?
15
+ raise NoSuchPropertyError, "No such property '#{method_name}'"
16
+ end
17
+
18
+ __load(matching_prop, method_name, @hash[method_name.to_s])
19
+ end
20
+
21
+ def respond_to_missing?(method_name, _include_private)
22
+ @complex_type.properties.any? { |p| p.name == method_name }
23
+ end
24
+
25
+ private
26
+
27
+ def __load(matching_prop, method_name, value)
28
+ return nil unless settable?(matching_prop)
29
+ return nil if value.nil?
30
+ return matching_prop.to_value(value, @context) if matching_prop.scalar?
31
+
32
+ if matching_prop.collection?
33
+ value.map { |v| __wrap(v, matching_prop.raw_type) }
34
+ else
35
+ __wrap(value, matching_prop.raw_type)
36
+ end
37
+ rescue InvalidValue => e
38
+ raise InvalidType, "The value provided for '#{method_name}' is of wrong type: #{e}"
39
+ end
40
+
41
+ def settable?(prop)
42
+ prop.public_send(SETTABLE_BY_OPERATION.fetch(@operation))
43
+ end
44
+
45
+ def __wrap(value, raw_type)
46
+ CreateComplexTypeHashWrapper.new(value, raw_type, operation: @operation, context: @context)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,79 @@
1
+ Class.new(MapperBuilder) do
2
+ def to_h
3
+ <% properties.each do |property| %>
4
+ <% if property.calling_method? %>
5
+ <%= property.name %>_value = calling_methods.fetch(:<%= property.method_name %>).call(obj)
6
+ <% elsif complex_type.respond_to?(:instance_methods) && complex_type.instance_methods.include?(property.method_name) %>
7
+ <%= property.name %>_value = wrapped_obj.<%= property.method_name %>
8
+ <% else %>
9
+ <%= property.name %>_value = obj.<%= property.method_name %>
10
+ <% end %>
11
+
12
+ <% if property.collection? %>
13
+ <% if property.scalar? %>
14
+ begin
15
+ <% if property.boolean? %>
16
+ <%= property.name %>_value&.map{ |rw| confirm_boolean("<%= property.name %>", rw) }
17
+ <% elsif property.string? %>
18
+ <%= property.name %>_value = <%= property.name %>_value&.map{ |v| v&.to_str }
19
+ <% elsif property.int? %>
20
+ <%= property.name %>_value = <%= property.name %>_value&.map{ |v| v&.to_int }
21
+ <% elsif property.date? %>
22
+ <%= property.name %>_value = <%= property.name %>_value&.map{ |v| v&.to_date&.iso8601 }
23
+ <% elsif property.datetime? %>
24
+ <%= property.name %>_value = <%= property.name %>_value&.map{ |v| v&.to_datetime&.iso8601 }
25
+ <% else %>
26
+ raise('<%= property.type %> Coming soon')
27
+ <% end %>
28
+ rescue NoMethodError => no__err
29
+ err = InvalidValue.new(no__err.message)
30
+ err.set_backtrace(no__err.backtrace.clone)
31
+ err.backtrace.unshift "<%= property.line__defined__at %>"
32
+ raise err
33
+ end
34
+ <% else %>
35
+ <%= property.name %>_value = <%= property.name %>_value&.map{ |rw| <%= property.name %>_mapper.obj_to_base_hash(rw) }
36
+ <% end %>
37
+ <% else %>
38
+ <% if property.scalar? %>
39
+ begin
40
+ <% if property.boolean? %>
41
+ <%= property.name %>_value = confirm_boolean("<%= property.name %>", <%= property.name %>_value)
42
+ <% elsif property.string? %>
43
+ <%= property.name %>_value = <%= property.name %>_value&.to_str
44
+ <% elsif property.int? %>
45
+ <%= property.name %>_value = <%= property.name %>_value&.to_int
46
+ <% elsif property.date? %>
47
+ <%= property.name %>_value = <%= property.name %>_value&.to_date&.iso8601
48
+ <% elsif property.datetime? %>
49
+ <%= property.name %>_value = <%= property.name %>_value&.to_datetime&.iso8601
50
+ <% elsif property.enum? %>
51
+ <%= property.name %>_value = confirm_one_of("<%= property.name %>", <%= property.name %>_value, %w[<%= property.enum_members.map(&:name).join(' ') %>])
52
+ <% else %>
53
+ raise('<%= property.type %> Coming soon')
54
+ <% end %>
55
+ rescue NoMethodError => no__err
56
+ err = InvalidValue.new(no__err.message)
57
+ err.set_backtrace(no__err.backtrace.clone)
58
+ err.backtrace.unshift "<%= property.line__defined__at %>"
59
+ raise err
60
+ end
61
+ <% else %>
62
+ <%= property.name %>_value = <%= property.name %>_mapper.obj_to_base_hash(<%= property.name %>_value)
63
+ <% end %>
64
+ <% end %>
65
+ <% end %>
66
+ {
67
+ <% properties.each do |property| %>
68
+ '<%= property.name %>' => <% unless property.nullable? %>not_nullable("<%= property.name %>",<% end %><%= property.name %>_value<% unless property.nullable? %>)<% end %>,
69
+ <% end %>
70
+ }
71
+ end
72
+
73
+ <% properties.reject(&:scalar?).each do |property| %>
74
+ # lazy so self/mutually-referencing types recurse per row of data, not per type
75
+ def <%= property.name %>_mapper
76
+ @<%= property.name %>_mapper ||= MapperBuilder.build(set_types.fetch(:<%= property.name %>))
77
+ end
78
+ <% end %>
79
+ end
@@ -0,0 +1,122 @@
1
+ require 'date'
2
+
3
+ module OdataDuty
4
+ class EdmBase
5
+ def self.scalar?
6
+ true
7
+ end
8
+ end
9
+
10
+ class EdmInt64 < EdmBase
11
+ def self.property_type
12
+ 'Edm.Int64'
13
+ end
14
+
15
+ OAS_TYPE = { 'type' => 'integer', 'format' => 'int64' }.freeze
16
+
17
+ def self.to_oas2(is_collection:)
18
+ return { 'type' => 'array', 'items' => OAS_TYPE } if is_collection
19
+
20
+ OAS_TYPE
21
+ end
22
+
23
+ def self.to_value(object, _context)
24
+ Integer(object)
25
+ rescue StandardError => e
26
+ raise InvalidValue, e
27
+ end
28
+ end
29
+
30
+ class EdmString < EdmBase
31
+ def self.property_type
32
+ 'Edm.String'
33
+ end
34
+
35
+ OAS_TYPE = { 'type' => 'string' }.freeze
36
+
37
+ def self.to_oas2(is_collection:)
38
+ return { 'type' => 'array', 'items' => OAS_TYPE } if is_collection
39
+
40
+ OAS_TYPE
41
+ end
42
+
43
+ def self.to_value(object, _context)
44
+ object.to_str
45
+ rescue StandardError => e
46
+ raise InvalidValue, e
47
+ end
48
+ end
49
+
50
+ class EdmDate < EdmBase
51
+ def self.property_type
52
+ 'Edm.Date'
53
+ end
54
+
55
+ OAS_TYPE = { 'type' => 'string', 'format' => 'date' }.freeze
56
+
57
+ def self.to_oas2(is_collection:)
58
+ return { 'type' => 'array', 'items' => OAS_TYPE } if is_collection
59
+
60
+ OAS_TYPE
61
+ end
62
+
63
+ def self.to_value(object, _context)
64
+ object.to_date.iso8601
65
+ rescue StandardError => e
66
+ raise InvalidValue, e
67
+ end
68
+ end
69
+
70
+ class EdmDateTimeOffset < EdmBase
71
+ def self.property_type
72
+ 'Edm.DateTimeOffset'
73
+ end
74
+
75
+ OAS_TYPE = { 'type' => 'string', 'format' => 'date-time' }.freeze
76
+
77
+ def self.to_oas2(is_collection:)
78
+ return { 'type' => 'array', 'items' => OAS_TYPE } if is_collection
79
+
80
+ OAS_TYPE
81
+ end
82
+
83
+ def self.to_value(object, _context)
84
+ object.to_datetime.iso8601
85
+ rescue StandardError => e
86
+ raise InvalidValue, e
87
+ end
88
+ end
89
+
90
+ class EdmBool < EdmBase
91
+ def self.property_type
92
+ 'Edm.Boolean'
93
+ end
94
+
95
+ OAS_TYPE = { 'type' => 'boolean' }.freeze
96
+
97
+ def self.to_oas2(is_collection:)
98
+ return { 'type' => 'array', 'items' => OAS_TYPE } if is_collection
99
+
100
+ OAS_TYPE
101
+ end
102
+
103
+ VALID_VALUES = [true, false, nil].freeze
104
+
105
+ def self.to_value(object, _context)
106
+ return true if object == 'true'
107
+ return false if object == 'false'
108
+ return object if VALID_VALUES.include?(object)
109
+
110
+ object.to_boolean
111
+ rescue NoMethodError
112
+ raise InvalidValue, "#{object} not boolean value"
113
+ end
114
+ end
115
+
116
+ TYPES_MAPPING = { Integer => EdmInt64,
117
+ String => EdmString,
118
+ Date => EdmDate,
119
+ DateTime => EdmDateTimeOffset,
120
+ Time => EdmDateTimeOffset,
121
+ TrueClass => EdmBool }.freeze
122
+ end
@@ -0,0 +1,19 @@
1
+ require 'erb'
2
+
3
+ module OdataDuty
4
+ module EdmxSchema
5
+ def self.index_hash(schema)
6
+ {
7
+ '@odata.context': [schema.base_url.chomp('/'), '$metadata'].join('/'),
8
+ value: schema.endpoints.map do |e|
9
+ { name: e.name, kind: 'EntitySet', url: e.url }
10
+ end
11
+ }
12
+ end
13
+
14
+ def self.metadata_xml(metadata)
15
+ b = binding
16
+ ERB.new(File.read("#{__dir__}/../metadata.xml.erb")).result(b)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ require_relative 'mapper_builder'
2
+
3
+ module OdataDuty
4
+ class EntityType < ComplexType
5
+ def self.property_refs
6
+ @property_refs ||= []
7
+ end
8
+
9
+ def self.property_ref(*args, **kwargs)
10
+ kwargs[:mutability] = :computed unless kwargs.key?(:mutability) || kwargs.key?(:computed)
11
+ property(*args, nullable: false, **kwargs).tap do |property|
12
+ raise 'Multiple Property Reference not yet supported' if property_refs.size.positive?
13
+
14
+ property_refs << property
15
+ end
16
+ end
17
+
18
+ class Metadata < ComplexType::Metadata
19
+ attr_reader :entity_type
20
+
21
+ def initialize(entity_type)
22
+ @entity_type = entity_type
23
+ super
24
+ end
25
+
26
+ def name
27
+ entity_type.to_s.split('::').last.sub(/EntityType\z/, '').sub(/Entity\z/, '')
28
+ end
29
+
30
+ def metadata_type
31
+ :entity
32
+ end
33
+
34
+ def property_refs
35
+ entity_type.property_refs
36
+ end
37
+ end
38
+
39
+ def self.__metadata
40
+ Metadata.new(self)
41
+ end
42
+
43
+ def od_endpoint
44
+ od_context.endpoint
45
+ end
46
+
47
+ def self.mapper(context, selected:)
48
+ context.current['odata_url_base'] ||= context.od_full_url(context.endpoint.url)
49
+ if property_refs.first.raw_type == EdmInt64
50
+ int_mapper(context, selected: selected)
51
+ else
52
+ string_mapper(context, selected: selected)
53
+ end
54
+ end
55
+
56
+ def self.int_mapper(context, selected:)
57
+ MapperBuilder.build(self, selected: selected) do |result, obj|
58
+ result['@odata.id'] = "#{context.current.fetch('odata_url_base')}(#{obj.id})"
59
+ end
60
+ end
61
+
62
+ def self.string_mapper(context, selected:)
63
+ MapperBuilder.build(self, selected: selected) do |result, obj|
64
+ result['@odata.id'] = "#{context.current.fetch('odata_url_base')}('#{obj.id}')"
65
+ end
66
+ end
67
+
68
+ def self._defined_at_
69
+ const_source_location(to_s).join(':')
70
+ end
71
+ end
72
+ end