Module: Ast::Merge::ProviderContract

Defined in:
lib/ast/merge/provider_contract.rb,
sig/ast/merge.rbs

Overview

Validation and normalization for the portable merge-provider protocol. rubocop:disable Metrics/ModuleLength -- protocol constants and validators form one public contract

Defined Under Namespace

Classes: DuplicateProviderError, Error, InvalidProviderError, InvalidRequestError, InvalidResultError

Constant Summary collapse

OPERATIONS =

Returns:

  • (Array[provider_operation])
%i[analyze diff2 merge2 merge3].freeze
REQUIRED_METHODS =

Returns:

  • (Array[Symbol])
%i[provider_id family capabilities analyze diff2 merge2 merge3].freeze
REQUEST_ROLES =

Returns:

  • (Hash[provider_operation, Array[Symbol]])
{
  analyze: %i[source],
  diff2: %i[before_source after_source],
  merge2: %i[incoming_source current_source],
  merge3: %i[base_source ours_source theirs_source]
}.freeze
REQUIRED_RESULT_FIELDS =

Returns:

  • (Array[Symbol])
%i[
  schema
  operation
  ok
  provider
  profile
  diagnostics
  changes
  conflicts
  fallbacks
  render_report
  verification
].freeze
REQUIRED_CAPABILITY_FIELDS =

Returns:

  • (Array[Symbol])
%i[
  operations
  dialects
  backends
  profiles
  role
  source_preservation
].freeze
RESULT_SCHEMA =

Returns:

  • (String)
'https://structuredmerge.org/schemas/provider-result/v1.json'
PROVIDER_ROLES =

Returns:

  • (Array[Symbol])
%i[workflow backend].freeze

Class Method Summary collapse

Class Method Details

.blank?(value) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/ast/merge/provider_contract.rb', line 105

def blank?(value)
  value.nil? || value.to_s.strip.empty?
end

.normalize_hash(value) ⇒ Object

Raises:

  • (ArgumentError)


88
89
90
91
92
93
94
95
# File 'lib/ast/merge/provider_contract.rb', line 88

def normalize_hash(value)
  raise ArgumentError, "Expected Hash, got #{value.class}" unless value.is_a?(Hash)

  value.to_h do |key, item|
    normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
    [normalized_key, item]
  end
end

.normalize_identifier(value) ⇒ Object



97
98
99
# File 'lib/ast/merge/provider_contract.rb', line 97

def normalize_identifier(value)
  value.to_s.strip
end

.normalize_identifiers(values) ⇒ Object



101
102
103
# File 'lib/ast/merge/provider_contract.rb', line 101

def normalize_identifiers(values)
  Array(values).map { |value| normalize_identifier(value).to_sym }.uniq.freeze
end

.normalize_operation(operation) ⇒ Object



79
80
81
82
83
84
85
86
# File 'lib/ast/merge/provider_contract.rb', line 79

def normalize_operation(operation)
  normalized = normalize_identifier(operation).to_sym
  unless OPERATIONS.include?(normalized)
    raise InvalidRequestError, "Unknown provider operation: #{operation.inspect}"
  end

  normalized
end

.normalized_capabilities(capabilities, operations:, role:) ⇒ Object



150
151
152
153
154
155
156
157
158
# File 'lib/ast/merge/provider_contract.rb', line 150

def normalized_capabilities(capabilities, operations:, role:)
  capabilities.merge(
    operations: operations,
    dialects: normalize_identifiers(capabilities[:dialects]),
    backends: normalize_identifiers(capabilities[:backends]),
    profiles: normalize_identifiers(capabilities[:profiles]),
    role: role
  ).freeze
end

.provider_registration(provider_id, family, capabilities, operations:, role:) ⇒ Object



160
161
162
163
164
165
166
# File 'lib/ast/merge/provider_contract.rb', line 160

def provider_registration(provider_id, family, capabilities, operations:, role:)
  {
    provider_id: provider_id,
    family: family,
    capabilities: normalized_capabilities(capabilities, operations: operations, role: role)
  }.freeze
end

.truthy_key?(value, key) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/ast/merge/provider_contract.rb', line 109

def truthy_key?(value, key)
  value.is_a?(Hash) && (value[key] == true || value[key.to_s] == true)
end

.validate_capability_fields!(capabilities) ⇒ Object



127
128
129
130
# File 'lib/ast/merge/provider_contract.rb', line 127

def validate_capability_fields!(capabilities)
  missing = REQUIRED_CAPABILITY_FIELDS.reject { |field| capabilities.key?(field) }
  raise InvalidProviderError, "Provider capabilities are missing: #{missing.join(', ')}" unless missing.empty?
end

.validate_operations!(values) ⇒ Object



132
133
134
135
136
137
138
139
140
141
# File 'lib/ast/merge/provider_contract.rb', line 132

def validate_operations!(values)
  operations = normalize_identifiers(values)
  unknown = operations - OPERATIONS
  raise InvalidProviderError, "Unknown provider operations: #{unknown.join(', ')}" unless unknown.empty?
  unless operations.sort == OPERATIONS.sort
    raise InvalidProviderError, "Provider must implement all operations: #{OPERATIONS.join(', ')}"
  end

  operations
end

.validate_provider!(provider) ⇒ Object



48
49
50
51
52
53
54
55
56
57
# File 'lib/ast/merge/provider_contract.rb', line 48

def validate_provider!(provider)
  validate_provider_methods!(provider)
  provider_id, family = validate_provider_identity!(provider)
  capabilities = normalize_hash(provider.capabilities)
  validate_capability_fields!(capabilities)
  operations = validate_operations!(capabilities[:operations])
  role = validate_role!(capabilities[:role])

  provider_registration(provider_id, family, capabilities, operations: operations, role: role)
end

.validate_provider_identity!(provider) ⇒ Object



118
119
120
121
122
123
124
125
# File 'lib/ast/merge/provider_contract.rb', line 118

def validate_provider_identity!(provider)
  provider_id = normalize_identifier(provider.provider_id)
  family = normalize_identifier(provider.family)
  raise InvalidProviderError, 'provider_id must not be empty' if provider_id.empty?
  raise InvalidProviderError, 'family must not be empty' if family.empty?

  [provider_id, family.to_sym]
end

.validate_provider_methods!(provider) ⇒ Object



113
114
115
116
# File 'lib/ast/merge/provider_contract.rb', line 113

def validate_provider_methods!(provider)
  missing = REQUIRED_METHODS.reject { |method_name| provider.respond_to?(method_name) }
  raise InvalidProviderError, "Provider is missing required methods: #{missing.join(', ')}" unless missing.empty?
end

.validate_request!(operation, request) ⇒ Object



59
60
61
62
63
64
65
66
67
68
# File 'lib/ast/merge/provider_contract.rb', line 59

def validate_request!(operation, request)
  operation = normalize_operation(operation)
  normalized = normalize_hash(request)
  validate_request_roles!(operation, normalized)
  if normalized[:provider_id].nil? && blank?(normalized[:family])
    raise InvalidRequestError, "#{operation} request requires family or provider_id"
  end

  normalized.freeze
end

.validate_request_roles!(operation, request) ⇒ Object



168
169
170
171
# File 'lib/ast/merge/provider_contract.rb', line 168

def validate_request_roles!(operation, request)
  missing = REQUEST_ROLES.fetch(operation).select { |role| !request.key?(role) || request[role].nil? }
  raise InvalidRequestError, "#{operation} request is missing roles: #{missing.join(', ')}" unless missing.empty?
end

.validate_result!(operation, result) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/ast/merge/provider_contract.rb', line 70

def validate_result!(operation, result)
  operation = normalize_operation(operation)
  normalized = normalize_hash(result)
  validate_result_fields!(operation, normalized)
  validate_result_operation!(operation, normalized[:operation])
  validate_result_outcome!(operation, normalized)
  normalized.freeze
end

.validate_result_fields!(operation, result) ⇒ Object

Raises:



173
174
175
176
# File 'lib/ast/merge/provider_contract.rb', line 173

def validate_result_fields!(operation, result)
  missing = REQUIRED_RESULT_FIELDS.reject { |field| result.key?(field) }
  raise InvalidResultError, "#{operation} result is missing fields: #{missing.join(', ')}" unless missing.empty?
end

.validate_result_operation!(expected, actual) ⇒ Object

Raises:



178
179
180
181
182
# File 'lib/ast/merge/provider_contract.rb', line 178

def validate_result_operation!(expected, actual)
  return if normalize_identifier(actual).to_sym == expected

  raise InvalidResultError, "Expected #{expected} result, got #{actual.inspect}"
end

.validate_result_outcome!(operation, result) ⇒ Object

Raises:



184
185
186
187
188
189
190
191
192
# File 'lib/ast/merge/provider_contract.rb', line 184

def validate_result_outcome!(operation, result)
  unless [true, false].include?(result[:ok])
    raise InvalidResultError, "#{operation} result ok must be true or false"
  end
  return unless operation == :merge3 && result[:ok]
  return if truthy_key?(result[:verification], :base_participated)

  raise InvalidResultError, 'Successful merge3 result must verify base_participated'
end

.validate_role!(value) ⇒ Object



143
144
145
146
147
148
# File 'lib/ast/merge/provider_contract.rb', line 143

def validate_role!(value)
  role = normalize_identifier(value).to_sym
  raise InvalidProviderError, "Unknown provider role: #{role}" unless PROVIDER_ROLES.include?(role)

  role
end