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.
- checksums.yaml +7 -0
- data/README.md +312 -0
- data/lib/generators/odata_duty/entity_set/entity_set_generator.rb +56 -0
- data/lib/generators/odata_duty/entity_set/templates/entity_set.rb.erb +20 -0
- data/lib/generators/odata_duty/entity_set/templates/entity_set_spec.rb.erb +38 -0
- data/lib/generators/odata_duty/entity_set/templates/entity_type.rb.erb +6 -0
- data/lib/generators/odata_duty/entity_set/templates/entity_type_spec.rb.erb +20 -0
- data/lib/generators/odata_duty/entity_set/templates/odata_active_record_concern.rb.erb +109 -0
- data/lib/generators/odata_duty/entity_set/templates/resolver.rb.erb +39 -0
- data/lib/generators/odata_duty/entity_set/templates/resolver_spec.rb.erb +50 -0
- data/lib/generators/odata_duty/install/install_generator.rb +60 -0
- data/lib/generators/odata_duty/install/templates/controller.rb.tt +43 -0
- data/lib/generators/odata_duty/install/templates/schema.rb.tt +16 -0
- data/lib/metadata.xml.erb +116 -0
- data/lib/odata_duty/complex_type.rb +70 -0
- data/lib/odata_duty/context_wrapper.rb +26 -0
- data/lib/odata_duty/create_complex_type_hash_wrapper.rb +49 -0
- data/lib/odata_duty/dynamic_object_wrapper.rb.erb +79 -0
- data/lib/odata_duty/edms.rb +122 -0
- data/lib/odata_duty/edmx_schema.rb +19 -0
- data/lib/odata_duty/entity_type.rb +72 -0
- data/lib/odata_duty/enum_type.rb +71 -0
- data/lib/odata_duty/errors.rb +52 -0
- data/lib/odata_duty/executor.rb +277 -0
- data/lib/odata_duty/filter.rb +76 -0
- data/lib/odata_duty/filter_predicate.rb +12 -0
- data/lib/odata_duty/mapper_builder.rb +86 -0
- data/lib/odata_duty/mcp_input_schemas.rb +59 -0
- data/lib/odata_duty/mcp_server_builder.rb +107 -0
- data/lib/odata_duty/oas2/collection_get_path.rb +97 -0
- data/lib/odata_duty/oas2/collection_post_path.rb +63 -0
- data/lib/odata_duty/oas2/individual_delete_path.rb +42 -0
- data/lib/odata_duty/oas2/individual_get_path.rb +37 -0
- data/lib/odata_duty/oas2/individual_patch_path.rb +63 -0
- data/lib/odata_duty/oas2.rb +113 -0
- data/lib/odata_duty/parslet_search_expression.rb +163 -0
- data/lib/odata_duty/property/collection_prop.rb +25 -0
- data/lib/odata_duty/property/single_prop.rb +131 -0
- data/lib/odata_duty/property.rb +49 -0
- data/lib/odata_duty/railtie.rb +11 -0
- data/lib/odata_duty/schema_builder/complex_type.rb +34 -0
- data/lib/odata_duty/schema_builder/container.rb +16 -0
- data/lib/odata_duty/schema_builder/data_type.rb +18 -0
- data/lib/odata_duty/schema_builder/endpoint.rb +117 -0
- data/lib/odata_duty/schema_builder/entity_set.rb +67 -0
- data/lib/odata_duty/schema_builder/entity_type.rb +49 -0
- data/lib/odata_duty/schema_builder/enum_type.rb +32 -0
- data/lib/odata_duty/schema_builder.rb +130 -0
- data/lib/odata_duty/set_resolver.rb +51 -0
- data/lib/odata_duty.rb +311 -0
- 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
|