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,25 @@
|
|
|
1
|
+
require_relative 'single_prop'
|
|
2
|
+
|
|
3
|
+
module OdataDuty
|
|
4
|
+
module Property
|
|
5
|
+
class CollectionProp < SingleProp
|
|
6
|
+
def convert(value, context)
|
|
7
|
+
value.map { |v| super(v, context) }
|
|
8
|
+
rescue NoMethodError
|
|
9
|
+
raise InvalidValue, "#{value} is not an collection"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_oas2_type
|
|
13
|
+
if scalar?
|
|
14
|
+
raw_type.to_oas2(is_collection: true)
|
|
15
|
+
else
|
|
16
|
+
{ 'type' => 'array', 'items' => { '$ref' => "#/definitions/#{raw_type.name}" } }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def collection?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module OdataDuty
|
|
2
|
+
module Property
|
|
3
|
+
CORE_ANNOTATION_TERMS = { computed: 'Org.OData.Core.V1.Computed',
|
|
4
|
+
immutable: 'Org.OData.Core.V1.Immutable' }.freeze
|
|
5
|
+
|
|
6
|
+
class SingleProp
|
|
7
|
+
attr_reader :name, :nullable, :calling_method, :line__defined__at, :raw_type, :type,
|
|
8
|
+
:set_type, :method_name, :mutability
|
|
9
|
+
|
|
10
|
+
def initialize(name, type, line__defined__at:, nullable:, method:, mutability:)
|
|
11
|
+
@line__defined__at = line__defined__at
|
|
12
|
+
@name = name.to_sym
|
|
13
|
+
@calling_method = method if method.respond_to?(:call)
|
|
14
|
+
method = nil if method.respond_to?(:call)
|
|
15
|
+
@method_name = (method || name).to_sym
|
|
16
|
+
@nullable = nullable ? true : false
|
|
17
|
+
@mutability = mutability
|
|
18
|
+
load_type_instance_vars(type)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def computed?
|
|
22
|
+
mutability == :computed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def immutable?
|
|
26
|
+
mutability == :immutable
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def non_insertable?
|
|
30
|
+
mutability == :non_insertable
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def core_annotation_term
|
|
34
|
+
CORE_ANNOTATION_TERMS[mutability]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def settable_on_create?
|
|
38
|
+
!computed? && !non_insertable?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def settable_on_update?
|
|
42
|
+
!computed? && !immutable?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def calling_method?
|
|
46
|
+
!calling_method.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def nullable?
|
|
50
|
+
nullable
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def boolean?
|
|
54
|
+
raw_type == EdmBool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def string?
|
|
58
|
+
raw_type == EdmString
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def int?
|
|
62
|
+
raw_type == EdmInt64
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def date?
|
|
66
|
+
raw_type == EdmDate
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def datetime?
|
|
70
|
+
raw_type == EdmDateTimeOffset
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def enum?
|
|
74
|
+
raw_type.respond_to?(:members)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def enum_members
|
|
78
|
+
raw_type.members
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scalar?
|
|
82
|
+
raw_type.scalar?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_value(value, context)
|
|
86
|
+
convert(value, context)
|
|
87
|
+
rescue InvalidValue => e
|
|
88
|
+
raise InvalidValue, "#{name} : #{e}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def convert(value, context)
|
|
92
|
+
return if value.nil?
|
|
93
|
+
|
|
94
|
+
@set_type.to_value(value, context)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def filter_convert(value, context)
|
|
98
|
+
convert(value, context)
|
|
99
|
+
rescue OdataDuty::InvalidValue
|
|
100
|
+
raise InvalidFilterValue, "Invalid value #{value} for #{name}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_oas2
|
|
104
|
+
to_oas2_type.dup.tap do |oas2|
|
|
105
|
+
oas2.merge!('readOnly' => true) if computed?
|
|
106
|
+
oas2.merge!('x-nullable' => true) if nullable
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_oas2_type
|
|
111
|
+
return raw_type.to_oas2(is_collection: false) if scalar? && !enum?
|
|
112
|
+
|
|
113
|
+
{ '$ref' => "#/definitions/#{raw_type.name}" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# nil (falsy) rather than false: callers only consume truthiness
|
|
117
|
+
def collection?; end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def load_type_instance_vars(type)
|
|
122
|
+
type = Array(type).first
|
|
123
|
+
@set_type = TYPES_MAPPING[type] || type
|
|
124
|
+
raise "Invalid type #{type.inspect} for #{name}" unless @set_type
|
|
125
|
+
|
|
126
|
+
@raw_type = @set_type.respond_to?(:__metadata) ? @set_type.__metadata : @set_type
|
|
127
|
+
@type = @raw_type.respond_to?(:property_type) ? @raw_type.property_type : @raw_type.name
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'odata_duty/edms'
|
|
2
|
+
require 'odata_duty/property/single_prop'
|
|
3
|
+
require 'odata_duty/property/collection_prop'
|
|
4
|
+
|
|
5
|
+
module OdataDuty
|
|
6
|
+
module Property
|
|
7
|
+
MUTABILITIES = %i[read_write immutable non_insertable computed].freeze
|
|
8
|
+
MUTABILITIES_LIST = MUTABILITIES.map(&:inspect).join(', ').freeze
|
|
9
|
+
|
|
10
|
+
def self.new(name, type = String, line__defined__at: nil, nullable: true, method: nil,
|
|
11
|
+
computed: :unset, mutability: :unset)
|
|
12
|
+
unless valid_name?(name)
|
|
13
|
+
raise InvalidNCNamesError, "\"#{name}\" is not a valid property name"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
prop_class = type.instance_of?(Array) ? CollectionProp : SingleProp
|
|
17
|
+
prop_class.new(name, type,
|
|
18
|
+
line__defined__at: line__defined__at,
|
|
19
|
+
nullable: nullable,
|
|
20
|
+
method: method,
|
|
21
|
+
mutability: resolve_mutability(name, computed, mutability))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.resolve_mutability(name, computed, mutability)
|
|
25
|
+
mutability = computed_to_mutability(name, computed, mutability) unless computed == :unset
|
|
26
|
+
mutability = :read_write if mutability == :unset
|
|
27
|
+
return mutability if MUTABILITIES.include?(mutability)
|
|
28
|
+
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"#{name}: invalid mutability #{mutability.inspect}, " \
|
|
31
|
+
"must be one of #{MUTABILITIES_LIST}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.computed_to_mutability(name, computed, mutability)
|
|
35
|
+
unless mutability == :unset
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"#{name}: pass either `mutability:` or `computed:`, not both \u2014 they control " \
|
|
38
|
+
'the same axis'
|
|
39
|
+
end
|
|
40
|
+
computed ? :computed : :read_write
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
NAME_REGEXP = Regexp.new('\A(?:\p{L}|_)(?:[\p{L}\p{Nd}_])*\z').freeze
|
|
44
|
+
|
|
45
|
+
def self.valid_name?(name)
|
|
46
|
+
name.match?(NAME_REGEXP)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require 'rails/railtie'
|
|
2
|
+
|
|
3
|
+
module OdataDuty
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
# Auto-load generators
|
|
6
|
+
generators do
|
|
7
|
+
require 'generators/odata_duty/install/install_generator'
|
|
8
|
+
require 'generators/odata_duty/entity_set/entity_set_generator'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'data_type'
|
|
2
|
+
require_relative '../mapper_builder'
|
|
3
|
+
|
|
4
|
+
module OdataDuty
|
|
5
|
+
module SchemaBuilder
|
|
6
|
+
class ComplexType < DataType
|
|
7
|
+
attr_reader :properties
|
|
8
|
+
|
|
9
|
+
def initialize(**kwargs)
|
|
10
|
+
super
|
|
11
|
+
@properties = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def property(name, *args, line__defined__at: caller.first, **kwargs)
|
|
15
|
+
if properties.any? { |p| p.name == name.to_sym }
|
|
16
|
+
raise PropertyAlreadyDefinedError, "#{name} is already defined"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Property.new(name, *args, line__defined__at: line__defined__at, **kwargs).tap do |property|
|
|
20
|
+
properties << property
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_oas2
|
|
25
|
+
{
|
|
26
|
+
'type' => 'object',
|
|
27
|
+
'properties' => properties.to_h do |property|
|
|
28
|
+
[property.name.to_s, property.to_oas2]
|
|
29
|
+
end
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module OdataDuty
|
|
2
|
+
module SchemaBuilder
|
|
3
|
+
class Container
|
|
4
|
+
attr_reader :name, :_defined_at_
|
|
5
|
+
|
|
6
|
+
def initialize(name:)
|
|
7
|
+
@name = name.clone
|
|
8
|
+
@_defined_at_ = caller.find { |line| !line.include?('/lib/odata_duty/') }
|
|
9
|
+
|
|
10
|
+
return if Property.valid_name?(@name)
|
|
11
|
+
|
|
12
|
+
raise InvalidNCNamesError, "\"#{@name}\" is not a valid property name"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module OdataDuty
|
|
2
|
+
module SchemaBuilder
|
|
3
|
+
class DataType
|
|
4
|
+
attr_reader :name, :_defined_at_
|
|
5
|
+
|
|
6
|
+
def initialize(name:)
|
|
7
|
+
@name = name.clone
|
|
8
|
+
@_defined_at_ = caller.find { |line| !line.include?('/lib/odata_duty/') }
|
|
9
|
+
|
|
10
|
+
return if Property.valid_name?(@name)
|
|
11
|
+
|
|
12
|
+
raise InvalidNCNamesError, "\"#{@name}\" is not a valid property name"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def scalar?; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module OdataDuty
|
|
2
|
+
module SchemaBuilder
|
|
3
|
+
class Endpoint
|
|
4
|
+
attr_reader :entity_set
|
|
5
|
+
|
|
6
|
+
def initialize(entity_set)
|
|
7
|
+
@entity_set = entity_set
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def name = entity_set.name
|
|
11
|
+
def url = entity_set.url
|
|
12
|
+
|
|
13
|
+
def new_entity_set(context:)
|
|
14
|
+
entity_set.resolver_class.new(context: context, init_args: entity_set.init_args)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def entity_type = entity_set.entity_type
|
|
18
|
+
|
|
19
|
+
def collection(set_builder, context:, selected:)
|
|
20
|
+
unless set_builder.respond_to?(:collection)
|
|
21
|
+
raise NoImplementationError, "collection not implemented for #{set_builder.class}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
values = set_builder.collection
|
|
25
|
+
mapper = entity_type.mapper(context, selected: selected)
|
|
26
|
+
|
|
27
|
+
# builder mapping never reads context; pass nil (accept equivalent mutant)
|
|
28
|
+
values.map { |v| mapper.obj_to_hash(v, nil) }
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
extend_error(e, set_builder, :collection)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def individual(set_builder, id, context:, selected:)
|
|
34
|
+
unless set_builder.respond_to?(:individual)
|
|
35
|
+
raise NoImplementationError, "individual not implemented for #{set_builder.class}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = set_builder.individual(converted_id(id))
|
|
39
|
+
raise ResourceNotFoundError, "No such entity #{id}" unless result
|
|
40
|
+
|
|
41
|
+
entity_type.mapper(context, selected: selected).obj_to_hash(result, nil)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
extend_error(e, set_builder, :individual)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create(context:)
|
|
47
|
+
wrapper = CreateComplexTypeHashWrapper.new(context.query_options, entity_type,
|
|
48
|
+
operation: :create, context: context)
|
|
49
|
+
result = new_entity_set(context: context).create(wrapper)
|
|
50
|
+
mapper = entity_type.mapper(context, selected: nil)
|
|
51
|
+
mapper.obj_to_hash(result, nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def supports_search?
|
|
55
|
+
entity_set.supports_search?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def supports_collection?
|
|
59
|
+
entity_set.supports_collection?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def supports_individual?
|
|
63
|
+
entity_set.supports_individual?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def supports_count?
|
|
67
|
+
entity_set.supports_count?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update(id, context:)
|
|
71
|
+
wrapper = CreateComplexTypeHashWrapper.new(context.query_options, entity_type,
|
|
72
|
+
operation: :update, context: context)
|
|
73
|
+
result = new_entity_set(context: context).update(converted_id(id), wrapper)
|
|
74
|
+
raise ResourceNotFoundError, "No such entity #{id}" unless result
|
|
75
|
+
|
|
76
|
+
entity_type.mapper(context, selected: nil).obj_to_hash(result, nil)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def delete(id, context:)
|
|
80
|
+
result = new_entity_set(context: context).delete(converted_id(id))
|
|
81
|
+
raise ResourceNotFoundError, "No such entity #{id}" unless result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def supports_create?
|
|
85
|
+
entity_set.supports_create?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def supports_update?
|
|
89
|
+
entity_set.supports_update?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def supports_delete?
|
|
93
|
+
entity_set.supports_delete?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def extend_error(err, set_builder, method_name)
|
|
99
|
+
klass = set_builder.class
|
|
100
|
+
err.backtrace.unshift(entity_set._defined_at_)
|
|
101
|
+
if set_builder.respond_to?(:od_after_init)
|
|
102
|
+
err.backtrace.unshift(klass.instance_method(:od_after_init).source_location.join(':'))
|
|
103
|
+
end
|
|
104
|
+
if set_builder.respond_to?(method_name)
|
|
105
|
+
err.backtrace.unshift(klass.instance_method(method_name).source_location.join(':'))
|
|
106
|
+
end
|
|
107
|
+
raise
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def converted_id(id)
|
|
111
|
+
entity_type.property_refs.first.convert(id, nil)
|
|
112
|
+
rescue OdataDuty::InvalidValue => e
|
|
113
|
+
raise InvalidPropertyReferenceValue, "Invalid individual id : #{e}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'delegate'
|
|
2
|
+
require_relative 'container'
|
|
3
|
+
|
|
4
|
+
module OdataDuty
|
|
5
|
+
module SchemaBuilder
|
|
6
|
+
class EntitySet < Container
|
|
7
|
+
attr_reader :entity_type, :url, :resolver, :init_args
|
|
8
|
+
|
|
9
|
+
def initialize(entity_type:, resolver:, name: nil, url: nil, init_args: nil)
|
|
10
|
+
@resolver = resolver.clone
|
|
11
|
+
name = name&.to_s || @resolver.split('::').last.sub(/Resolver\z/, '')
|
|
12
|
+
super(name: name)
|
|
13
|
+
@url = (url&.to_s || @name).clone
|
|
14
|
+
@entity_type = entity_type
|
|
15
|
+
@init_args = init_args
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def entity_type_name = entity_type.name
|
|
19
|
+
|
|
20
|
+
def resolver_class = Module.const_get(resolver)
|
|
21
|
+
|
|
22
|
+
def supports_search?
|
|
23
|
+
# Check if the resolver class supports search by looking for the od_search method
|
|
24
|
+
resolver_class.method_defined?(:od_search)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def supports_filter_or?
|
|
28
|
+
resolver_class.method_defined?(:od_filter_or)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def supports_collection?
|
|
32
|
+
# Check if the resolver class supports read by looking for the collection method
|
|
33
|
+
resolver_class.method_defined?(:collection)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supports_individual?
|
|
37
|
+
# Check if the resolver class supports read-by-id via the individual method
|
|
38
|
+
resolver_class.method_defined?(:individual)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def supports_count?
|
|
42
|
+
# Check if the resolver class supports counting via the count method
|
|
43
|
+
resolver_class.method_defined?(:count)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def supports_create?
|
|
47
|
+
# Check if the resolver class supports create by looking for the create method
|
|
48
|
+
resolver_class.method_defined?(:create)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def supports_update?
|
|
52
|
+
# Check if the resolver class supports update by looking for the update method
|
|
53
|
+
resolver_class.method_defined?(:update)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def supports_delete?
|
|
57
|
+
# Check if the resolver class supports delete by looking for the delete method
|
|
58
|
+
resolver_class.method_defined?(:delete)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def non_insertable_property_names
|
|
62
|
+
@non_insertable_property_names ||=
|
|
63
|
+
entity_type.properties.select(&:non_insertable?).map(&:name)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require_relative 'complex_type'
|
|
2
|
+
|
|
3
|
+
module OdataDuty
|
|
4
|
+
module SchemaBuilder
|
|
5
|
+
class EntityType < ComplexType
|
|
6
|
+
attr_reader :property_refs
|
|
7
|
+
|
|
8
|
+
def initialize(**kwargs)
|
|
9
|
+
super
|
|
10
|
+
@property_refs = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def property_ref(*args, **kwargs)
|
|
14
|
+
kwargs[:mutability] = :computed unless kwargs.key?(:mutability) || kwargs.key?(:computed)
|
|
15
|
+
property(*args, nullable: false, **kwargs).tap do |property|
|
|
16
|
+
raise 'Multiple Property Reference not yet supported' if property_refs.size.positive?
|
|
17
|
+
|
|
18
|
+
property_refs << property
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mapper(context, selected:)
|
|
23
|
+
context.current['odata_url_base'] ||= context.od_full_url(context.endpoint.url)
|
|
24
|
+
if integer_property_ref?
|
|
25
|
+
int_mapper(context, selected: selected)
|
|
26
|
+
else
|
|
27
|
+
string_mapper(context, selected: selected)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def int_mapper(context, selected:)
|
|
32
|
+
MapperBuilder.build(self, selected: selected) do |result, obj|
|
|
33
|
+
result['@odata.id'] = "#{context.current.fetch('odata_url_base')}(#{obj.id})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def string_mapper(context, selected:)
|
|
38
|
+
MapperBuilder.build(self, selected: selected) do |result, obj|
|
|
39
|
+
result['@odata.id'] = "#{context.current.fetch('odata_url_base')}('#{obj.id}')"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def integer_property_ref?
|
|
44
|
+
@property_ref_raw_type ||= property_refs.first.raw_type
|
|
45
|
+
@property_ref_raw_type == EdmInt64
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative 'data_type'
|
|
2
|
+
|
|
3
|
+
module OdataDuty
|
|
4
|
+
module SchemaBuilder
|
|
5
|
+
class EnumType < DataType
|
|
6
|
+
attr_reader :members
|
|
7
|
+
|
|
8
|
+
def initialize(**kwargs)
|
|
9
|
+
super
|
|
10
|
+
@members = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def member(*)
|
|
14
|
+
@members << EnumMember.new(*)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scalar?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_value(val, _context)
|
|
22
|
+
return val if members.map(&:name).include?(val)
|
|
23
|
+
|
|
24
|
+
raise InvalidValue, "#{val} is not a valid member of #{members}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_oas2
|
|
28
|
+
{ 'type' => 'string', 'enum' => members.map(&:name) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require_relative 'schema_builder/enum_type'
|
|
2
|
+
require_relative 'schema_builder/complex_type'
|
|
3
|
+
require_relative 'schema_builder/entity_type'
|
|
4
|
+
require_relative 'schema_builder/entity_set'
|
|
5
|
+
require_relative 'schema_builder/endpoint'
|
|
6
|
+
|
|
7
|
+
module OdataDuty
|
|
8
|
+
module SchemaBuilder
|
|
9
|
+
def self.build(**kwargs, &block)
|
|
10
|
+
Schema.new(**kwargs).tap { |s| block.call(s) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Schema
|
|
14
|
+
attr_reader :namespace, :host, :scheme, :base_path, :base_url, :types
|
|
15
|
+
attr_accessor :version, :title
|
|
16
|
+
|
|
17
|
+
def initialize(namespace:, host: 'localhost', scheme: 'https', base_path: '')
|
|
18
|
+
@namespace = namespace.to_str.clone.freeze
|
|
19
|
+
@host = host.to_str.clone.freeze
|
|
20
|
+
@scheme = scheme.to_str.clone.freeze
|
|
21
|
+
@base_path = base_path.to_str.clone.freeze
|
|
22
|
+
@base_url = [scheme, '://', host, base_path].join.freeze
|
|
23
|
+
@types = {}
|
|
24
|
+
@containers = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inspect
|
|
28
|
+
"#<#{self.class} @namespace=#{@namespace} @base_path=#{@base_path} \
|
|
29
|
+
containers=#{@containers.keys} types=#{@types.keys}>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_complex_type(**kwargs, &block)
|
|
33
|
+
ComplexType.new(**kwargs).tap do |complex_type|
|
|
34
|
+
add_type complex_type
|
|
35
|
+
block.call(complex_type)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_enum_type(**kwargs, &block)
|
|
40
|
+
EnumType.new(**kwargs).tap do |enum_type|
|
|
41
|
+
add_type enum_type
|
|
42
|
+
block.call(enum_type)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_entity_type(**kwargs, &block)
|
|
47
|
+
EntityType.new(**kwargs).tap do |entity_type|
|
|
48
|
+
add_type entity_type
|
|
49
|
+
block.call(entity_type)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_entity_set(**kwargs)
|
|
54
|
+
EntitySet.new(**kwargs).tap { |entity_set| add_container(entity_set) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def endpoints
|
|
58
|
+
entity_sets.map { |entity_set| Endpoint.new(entity_set) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def enum_types
|
|
62
|
+
all_types.grep(EnumType)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def complex_types
|
|
66
|
+
all_types.select { |t| t.instance_of?(ComplexType) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def entity_types
|
|
70
|
+
all_types.grep(EntityType)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entity_sets = all_containers
|
|
74
|
+
|
|
75
|
+
def collection_entity_sets
|
|
76
|
+
entity_sets.select { |t| t.resolver_class.method_defined?(:collection) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def individual_entity_sets
|
|
80
|
+
entity_sets.select { |t| t.resolver_class.method_defined?(:individual) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute(url, context:, query_options: {})
|
|
84
|
+
Executor.execute(url: url, context: context, query_options: query_options, schema: self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create(url, context:, query_options: nil)
|
|
88
|
+
Executor.create(url: url, context: context, query_options: query_options, schema: self)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def update(url, context:, query_options: nil)
|
|
92
|
+
Executor.update(url: url, context: context, query_options: query_options, schema: self)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def delete(url, context:, query_options: nil)
|
|
96
|
+
Executor.delete(url: url, context: context, query_options: query_options, schema: self)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_mcp_server
|
|
100
|
+
McpServerBuilder.build(self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def metadata_xml
|
|
104
|
+
EdmxSchema.metadata_xml(self)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def index_hash
|
|
108
|
+
EdmxSchema.index_hash(self)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def add_type(type)
|
|
114
|
+
raise "Duplicate #{type.name} type" if @types.key?(type.name)
|
|
115
|
+
|
|
116
|
+
@types[type.name] = type
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def all_types = @types.values.sort_by(&:name)
|
|
120
|
+
|
|
121
|
+
def add_container(type)
|
|
122
|
+
raise "Duplicate #{type.name} Container" if @containers.key?(type.name)
|
|
123
|
+
|
|
124
|
+
@containers[type.name] = type
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def all_containers = @containers.values.sort_by(&:name)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|