Skip to content

Commit 4532f0e

Browse files
committed
Extract build_single_operation and OperationInfo delegators
Refactors the large build_operations loop into two focused methods (build_single_operation, populate_operation_metadata) and adds input?, rpc_input_namespace, rpc_output_namespace delegators to OperationInfo so the builder goes through the facade instead of reaching into binding internals. * Add builder edge-case tests: missing portType op, missing input element, overloaded operations, unresolved binding reference * Add OperationInfo spec covering the new delegator methods
1 parent 3504c39 commit 4532f0e

4 files changed

Lines changed: 363 additions & 52 deletions

File tree

lib/wsdl/definition/builder.rb

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -88,72 +88,100 @@ def build_services
8888

8989
# Builds operations for a given port.
9090
#
91-
# Resolves binding and port_type once, then iterates all operations
92-
# directly.
91+
# Resolves binding and port_type once, then delegates each operation
92+
# to {#build_single_operation}.
9393
#
9494
# @param port [Parser::Port] the port
9595
# @return [Hash{String => Hash, Array<Hash>}] operation data keyed by name
96-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
9796
def build_operations(port)
9897
binding = port.fetch_binding(@documents)
9998
port_type = binding.fetch_port_type(@documents)
10099
operations = {}
101100
element_builder = XML::ElementBuilder.new(@schemas, limits: @limits, issues: @build_issues)
102101

103102
binding.operations.to_a.each do |op_entry|
104-
op_name = op_entry[:name]
105-
input_name = op_entry[:input_name]
106-
metadata = default_operation(op_name, input_name:)
107-
108-
binding_op = binding.operations.fetch(op_name, input_name:)
109-
port_type_op = port_type.operations.fetch(op_name, input_name:) { nil }
110-
111-
unless port_type_op
112-
record_build_issue(op_name,
113-
"Binding operation #{op_name.inspect} not found in portType #{port_type.name.inspect}")
114-
store_operation(operations, op_name, metadata.freeze)
115-
next
116-
end
117-
118-
op_info = Parser::OperationInfo.new(
119-
op_name, binding_op, port_type_op,
120-
documents: @documents, schemas: @schemas,
121-
limits: @limits, issues: @build_issues,
122-
element_builder:
123-
)
124-
125-
metadata[:soap_action] = op_info.soap_action
126-
metadata[:soap_version] = op_info.soap_version
127-
128-
if binding_op.input?
129-
metadata[:input_style] = op_info.input_style
130-
metadata[:output_style] = op_info.output_style
131-
metadata[:rpc_input_namespace] = binding_op.input_body[:namespace]
132-
metadata[:rpc_output_namespace] = binding_op.output_body&.dig(:namespace)
133-
else
134-
record_build_issue(op_name,
135-
"Binding operation #{op_name.inspect} is missing a required <input> element")
136-
end
137-
138-
metadata[:schema_complete] = schema_complete_for_operation?(op_info)
139-
metadata[:input] = build_message(op_info.input)
140-
metadata[:output] = op_info.output ? build_message(op_info.output) : nil
141-
store_operation(operations, op_name, metadata.freeze)
103+
metadata = build_single_operation(op_entry, binding, port_type, element_builder)
104+
store_operation(operations, op_entry[:name], metadata.freeze)
142105
end
143106

144107
operations
145108
rescue UnresolvedReferenceError => e
146109
record_build_issue(nil, e.message)
147110
{}
148111
end
149-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
112+
113+
# Builds a single operation's metadata hash.
114+
#
115+
# Resolves binding and port type operations, validates the port type
116+
# match, and populates the metadata from the resolved operation info.
117+
# Returns a default metadata hash when the port type operation is missing.
118+
#
119+
# @param op_entry [Hash] operation entry from {Parser::OperationMap#to_a}
120+
# @param binding [Parser::Binding] the resolved binding
121+
# @param port_type [Parser::PortType] the resolved port type
122+
# @param element_builder [XML::ElementBuilder] shared element builder
123+
# @return [Hash] operation metadata
124+
def build_single_operation(op_entry, binding, port_type, element_builder)
125+
op_name = op_entry[:name]
126+
metadata = default_operation(op_name, input_name: op_entry[:input_name])
127+
128+
binding_op = binding.operations.fetch(op_name, input_name: op_entry[:input_name])
129+
port_type_op = port_type.operations.fetch(op_name, input_name: op_entry[:input_name]) { nil }
130+
131+
unless port_type_op
132+
record_build_issue(op_name,
133+
"Binding operation #{op_name.inspect} not found in portType #{port_type.name.inspect}")
134+
return metadata
135+
end
136+
137+
op_info = Parser::OperationInfo.new(
138+
op_name, binding_op, port_type_op,
139+
documents: @documents, schemas: @schemas,
140+
limits: @limits, issues: @build_issues,
141+
element_builder:
142+
)
143+
144+
populate_operation_metadata(metadata, op_name, op_info)
145+
metadata
146+
end
147+
148+
# Populates an operation metadata hash from resolved operation info.
149+
#
150+
# Sets SOAP protocol fields, binding styles, schema completeness,
151+
# and resolved input/output messages. All data is accessed through the
152+
# {Parser::OperationInfo} facade rather than reaching into lower-level
153+
# binding or port type objects directly.
154+
#
155+
# @param metadata [Hash] the operation metadata hash to populate
156+
# @param op_name [String] the operation name (for error reporting)
157+
# @param op_info [Parser::OperationInfo] the resolved operation info
158+
# @return [void]
159+
# rubocop:disable Metrics/AbcSize -- data-mapping method; high ABC from 9 hash assignments, not complexity
160+
def populate_operation_metadata(metadata, op_name, op_info)
161+
metadata[:soap_action] = op_info.soap_action
162+
metadata[:soap_version] = op_info.soap_version
163+
164+
if op_info.input?
165+
metadata[:input_style] = op_info.input_style
166+
metadata[:output_style] = op_info.output_style
167+
metadata[:rpc_input_namespace] = op_info.rpc_input_namespace
168+
metadata[:rpc_output_namespace] = op_info.rpc_output_namespace
169+
else
170+
record_build_issue(op_name,
171+
"Binding operation #{op_name.inspect} is missing a required <input> element")
172+
end
173+
174+
metadata[:schema_complete] = schema_complete_for_operation?(op_info)
175+
metadata[:input] = build_message(op_info.input)
176+
metadata[:output] = op_info.output ? build_message(op_info.output) : nil
177+
end
178+
# rubocop:enable Metrics/AbcSize
150179

151180
# Returns an operation hash with safe defaults.
152181
#
153-
# Each field starts as nil/empty. The loop in {#build_operations}
154-
# progressively enhances the hash with binding-level metadata and
155-
# resolved message data. If any step fails, the rescue stores
156-
# whatever was captured up to that point.
182+
# Each field starts as nil/empty. {#build_single_operation} and
183+
# {#populate_operation_metadata} progressively enhance the hash
184+
# with binding-level metadata and resolved message data.
157185
#
158186
# @param name [String] the operation name
159187
# @param input_name [String, nil] disambiguator for overloaded operations

lib/wsdl/parser/operation_info.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@ def initialize(name, binding_operation, port_type_operation,
3838
# @return [String] the name of this operation
3939
attr_reader :name
4040

41-
# @return [BindingOperation] the binding operation with protocol details
42-
attr_reader :binding_operation
43-
44-
# @return [PortTypeOperation] the port type operation with interface details
45-
attr_reader :port_type_operation
46-
4741
# Returns the SOAP action for this operation.
4842
#
4943
# The SOAP action is typically used as an HTTP header value to indicate
@@ -99,6 +93,27 @@ def output
9993
)
10094
end
10195

96+
# Returns whether the binding operation defines an input element.
97+
#
98+
# @return [Boolean] true if the binding has an +<input>+ element
99+
def input?
100+
@binding_operation.input?
101+
end
102+
103+
# Returns the RPC input namespace from the binding's input body.
104+
#
105+
# @return [String, nil] the namespace URI, or nil if not specified
106+
def rpc_input_namespace
107+
@binding_operation.input_body[:namespace]
108+
end
109+
110+
# Returns the RPC output namespace from the binding's output body.
111+
#
112+
# @return [String, nil] the namespace URI, or nil if not specified
113+
def rpc_output_namespace
114+
@binding_operation.output_body[:namespace]
115+
end
116+
102117
# Returns the input style for this operation.
103118
#
104119
# The style is a combination of the binding style and use attribute,

spec/wsdl/definition/builder_spec.rb

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,146 @@
208208
end
209209
end
210210

211+
describe 'build_operations edge cases' do
212+
let(:tempfiles) { [] }
213+
214+
after do
215+
tempfiles.each(&:close!)
216+
end
217+
218+
def write_wsdl_file(wsdl_xml)
219+
file = Tempfile.new(['builder-spec', '.wsdl'])
220+
file.write(wsdl_xml)
221+
file.flush
222+
tempfiles << file
223+
file.path
224+
end
225+
226+
def parse_definition(wsdl_xml)
227+
WSDL::Parser.parse(write_wsdl_file(wsdl_xml), http_mock)
228+
end
229+
230+
describe 'binding operation not in portType' do
231+
subject(:definition) do
232+
parse_definition(<<~XML)
233+
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
234+
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
235+
xmlns:tns="http://t.com" targetNamespace="http://t.com">
236+
<portType name="PT"/>
237+
<binding name="B" type="tns:PT">
238+
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
239+
<operation name="GhostOp">
240+
<soap:operation soapAction="ghost"/>
241+
<input><soap:body use="literal"/></input>
242+
<output><soap:body use="literal"/></output>
243+
</operation>
244+
</binding>
245+
<service name="S">
246+
<port name="P" binding="tns:B"><soap:address location="http://x.com"/></port>
247+
</service>
248+
</definitions>
249+
XML
250+
end
251+
252+
it 'records a build issue referencing the portType' do
253+
expect(definition.build_issues).to contain_exactly(
254+
a_hash_including(type: :build_error, operation: 'GhostOp')
255+
)
256+
expect(definition.build_issues.first[:error]).to include('portType')
257+
end
258+
259+
it 'stores the operation with default metadata' do
260+
op = definition.operation_data('S', 'P', 'GhostOp')
261+
262+
expect(op[:name]).to eq('GhostOp')
263+
expect(op[:soap_action]).to be_nil
264+
expect(op[:input_style]).to be_nil
265+
expect(op[:input]).to eq(header: [], body: [])
266+
expect(op[:output]).to be_nil
267+
end
268+
end
269+
270+
describe 'binding operation missing input element' do
271+
subject(:definition) do
272+
parse_definition(<<~XML)
273+
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
274+
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
275+
xmlns:tns="http://t.com" targetNamespace="http://t.com">
276+
<portType name="PT"><operation name="Op"/></portType>
277+
<binding name="B" type="tns:PT">
278+
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
279+
<operation name="Op"><soap:operation soapAction="DoStuff"/></operation>
280+
</binding>
281+
<service name="S">
282+
<port name="P" binding="tns:B"><soap:address location="http://x.com"/></port>
283+
</service>
284+
</definitions>
285+
XML
286+
end
287+
288+
it 'records a build issue about missing input' do
289+
expect(definition.build_issues).to include(
290+
a_hash_including(
291+
type: :build_error,
292+
operation: 'Op',
293+
error: a_string_matching(/missing a required.*input/i)
294+
)
295+
)
296+
end
297+
298+
it 'populates soap_action and soap_version but not styles' do
299+
op = definition.operation_data('S', 'P', 'Op')
300+
301+
expect(op[:soap_action]).to eq('DoStuff')
302+
expect(op[:soap_version]).to eq('1.1')
303+
expect(op[:input_style]).to be_nil
304+
expect(op[:output_style]).to be_nil
305+
end
306+
end
307+
308+
describe 'overloaded operations' do
309+
subject(:definition) do
310+
WSDL::Parser.parse(fixture('parser/operation_overloading'), http_mock)
311+
end
312+
313+
it 'stores overloaded operations as an array' do
314+
ops = definition.to_h.dig('services', 'LookupService', 'ports', 'LookupPort', 'operations')
315+
lookup = ops['Lookup']
316+
317+
expect(lookup).to be_an(Array)
318+
expect(lookup.size).to eq(2)
319+
expect(lookup.map { |o| o['input_name'] }).to contain_exactly('LookupById', 'LookupByName')
320+
end
321+
322+
it 'assigns distinct metadata to each overload' do
323+
by_id = definition.operation_data('LookupService', 'LookupPort', 'Lookup', input_name: 'LookupById')
324+
by_name = definition.operation_data('LookupService', 'LookupPort', 'Lookup', input_name: 'LookupByName')
325+
326+
expect(by_id[:soap_action]).to eq('LookupById')
327+
expect(by_name[:soap_action]).to eq('LookupByName')
328+
expect(by_id[:input][:body]).not_to eq(by_name[:input][:body])
329+
end
330+
end
331+
332+
describe 'unresolved binding reference' do
333+
subject(:definition) do
334+
WSDL::Parser.parse(fixture('parser/unresolved_references/binding'), http_mock)
335+
end
336+
337+
it 'records a build issue with nil operation' do
338+
expect(definition.build_issues).to include(
339+
a_hash_including(type: :build_error, operation: nil)
340+
)
341+
end
342+
343+
it 'returns empty operations for the affected port' do
344+
port = definition.to_h.dig('services', 'BadService', 'ports', 'BadPort')
345+
346+
expect(port['operations']).to be_empty
347+
end
348+
end
349+
end
350+
211351
private
212352

213353
def collect_types(elements)

0 commit comments

Comments
 (0)