diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0422bc..171679b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,3 @@ jobs: bundler-cache: true - name: Run the default task run: bundle exec rake - - - name: Run Steep check - run: bundle exec steep check diff --git a/lib/wampproto/message.rb b/lib/wampproto/message.rb index a9f2cfe..c874040 100644 --- a/lib/wampproto/message.rb +++ b/lib/wampproto/message.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative "message/exceptions" +require_relative "message/util" +require_relative "message/validation_spec" + require_relative "message/base" require_relative "message/hello" require_relative "message/welcome" diff --git a/lib/wampproto/message/abort.rb b/lib/wampproto/message/abort.rb index 460f54c..edd38c0 100644 --- a/lib/wampproto/message/abort.rb +++ b/lib/wampproto/message/abort.rb @@ -2,16 +2,65 @@ module Wampproto module Message + # interface for abort fields + class IAbortFields + def details + raise NotImplementedError + end + + def reason + raise NotImplementedError + end + + def args + raise NotImplementedError + end + + def kwargs + raise NotImplementedError + end + end + + # abort fields + class AbortFields < IAbortFields + attr_reader :details, :reason, :args, :kwargs + + def initialize(details, reason, *args, **kwargs) + super() + @details = details + @reason = reason + @args = args + @kwargs = kwargs + end + end + # abort message class Abort < Base attr_reader :details, :reason, :args, :kwargs + TEXT = "ABORT" + VALIDATION_SPEC = Message::ValidationSpec.new( + 3, + 5, + TEXT, + { + 1 => Message::Util.method(:validate_details), + 2 => Message::Util.method(:validate_reason), + 3 => Message::Util.method(:validate_args), + 4 => Message::Util.method(:validate_kwargs) + } + ) + def initialize(details, reason, *args, **kwargs) super() - @details = Validate.hash!("Details", details) - @reason = Validate.string!("Reason", reason) - @args = Validate.array!("Arguments", args) - @kwargs = Validate.hash!("Keyword Arguments", kwargs) + @details = details + @reason = reason + @args = args + @kwargs = kwargs + end + + def self.with_fields(fields) + new(fields.details, fields.reason, *fields.args, **fields.kwargs) end def marshal @@ -22,10 +71,10 @@ def marshal end def self.parse(wamp_message) - _type, details, reason, args, kwargs = wamp_message - args ||= [] - kwargs ||= {} - new(details, reason, *args, **kwargs) + fields = Util.validate_message(wamp_message, Type::ABORT, VALIDATION_SPEC) + fields.args ||= [] + fields.kwargs ||= {} + Abort.with_fields(fields) end end end diff --git a/lib/wampproto/message/exceptions.rb b/lib/wampproto/message/exceptions.rb new file mode 100644 index 0000000..55ea059 --- /dev/null +++ b/lib/wampproto/message/exceptions.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Wampproto + module Message + module Exceptions + INVALID_DATA_TYPE_ERROR = "%s: value at index %s must be of type '%s'" \ + "but was %s" + INVALID_RANGE_ERROR = "%s: value at index %s must be between '%s' and '%s" \ + "but was %s" + INVALID_DETAIL_ERROR = "%s: value at index %s for key '%s' must be of type" \ + "'%s' but was %s" + end + end +end diff --git a/lib/wampproto/message/util.rb b/lib/wampproto/message/util.rb new file mode 100644 index 0000000..273ea07 --- /dev/null +++ b/lib/wampproto/message/util.rb @@ -0,0 +1,469 @@ +# frozen_string_literal: true + +module Wampproto + # message module + module Message + # fields + class Fields + attr_accessor :request_id, :uri, :args, :kwargs, :session_id, :realm, :authid, + :authrole, :authmethod, :authmethods, :authextra, :roles, + :message_type, :signature, :reason, :topic, :extra, + :options, :details, :subscription_id, :publication_id, + :registration_id + end + + # util module + module Util # rubocop:disable Metrics/ModuleLength + MIN_ID = 1 + MAX_ID = 1 << 53 + DEFAULT_ROLES = { caller: {}, publisher: {}, subscriber: {}, callee: {} }.freeze + + module_function + + def validate_int(value, index, message) + return nil if value.is_a?(Integer) + + format( + Exceptions::INVALID_DATA_TYPE_ERROR, + message:, + index:, + expected_type: "int", + actual_type: value.class + ) + end + + def validate_string(value, index, message) + return nil if value.is_a?(String) + + format( + Exceptions::INVALID_DATA_TYPE_ERROR, + message:, + index:, + expected_type: "string", + actual_type: value.class + ) + end + + def validate_list(value, index, message) + return nil if value.is_a?(Array) + + format( + Exceptions::INVALID_DATA_TYPE_ERROR, + message:, + index:, + expected_type: "list", + actual_type: value.class + ) + end + + def validate_hash(value, index, message) + return nil if value.is_a?(Hash) + + format( + Exceptions::INVALID_DATA_TYPE_ERROR, + message:, + index:, + expected_type: "hash", + actual_type: value.class + ) + end + + def validate_id(value, index, message) # rubocop: disable Metrics/MethodLength + error = validate_int(value, index, message) + return error if error + + if (value < MIN_ID) || (value > MAX_ID) + return format( + Exceptions::INVALID_RANGE_ERROR, + message:, + index:, + start: MIN_ID, + end: MAX_ID, + actual: value + ) + end + + nil + end + + def validate_request_id(wamp_msg, index, fields, message) + error = validate_id(wamp_msg[index], index, message) + + return error if error + + fields.request_id = wamp_msg[index] + nil + end + + def validate_uri(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.uri = wamp_msg[index] + nil + end + + def validate_args(wamp_msg, index, fields, message) + if wamp_msg.length > index + error = validate_list(wamp_msg[index], index, message) + + return error if error + + fields.args = wamp_msg[index] + end + nil + end + + def validate_kwargs(wamp_msg, index, fields, message) + if wamp_msg.length > index + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + fields.kwargs = wamp_msg[index] + end + nil + end + + def validate_session_id(wamp_msg, index, fields, message) + error = validate_id(wamp_msg[index], index, message) + + return error if error + + fields.session_id = wamp_msg[index] + nil + end + + def validate_realm(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.realm = wamp_msg[index] + nil + end + + def validate_authid(details, index, fields, message) # rubocop: disable Metrics/MethodLength + if details.key?("authid") + authid = details["authid"] + error = validate_string(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authid", + expected_type: "string", + actual_type: authid.class + ) + + return new_error if error + + fields.authid = authid + end + + nil + end + + def validate_authrole(details, index, fields, message) # rubocop:disable Metrics/MethodLength + if details.key?("authrole") + authrole = details["authrole"] + error = validate_string(authrole, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authrole", + expected_type: "string", + actual_type: authrole.class + ) + + return new_error if error + + fields.authrole = authrole + end + + nil + end + + def validate_authmethod(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.authmethod = wamp_msg[index] + nil + end + + def validate_authmethods(details, index, fields, message) # rubocop:disable Metrics/MethodLength + if details.key?("authmethods") + authmethods = details["authmethods"] + error = validate_list(authmethods, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authmethods", + expected_type: "list", + actual_type: authmethods.class + ) + + return new_error if error + + fields.authmethods = authmethods + end + + nil + end + + def validate_welcome_authmethod(details, index, fields, message) # rubocop:disable Metrics/MethodLength + if details.key?("authmethod") + authmethod = details["authmethod"] + error = validate_string(authmethod, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authmethod", + expected_type: "string", + actual_type: authmethod.class + ) + + return new_error if error + + fields.authmethod = authmethod + end + + nil + end + + def validate_authextra(details, index, fields, message) # rubocop:disable Metrics/MethodLength + if details.key?("authextra") + authextra = details["authextra"] + error = validate_hash(authextra, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authextra", + expected_type: "hash", + actual_type: authextra.class + ) + + return new_error if error + + fields.authextra = authextra + end + + nil + end + + def validate_roles(details, index, fields, message) # rubocop:disable Metrics/MethodLength + if details.key?("roles") + roles = details["roles"] + error = validate_hash(roles, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "roles", + expected_type: "hash", + actual_type: roles.class + ) + + return new_error if error + + valid_keys = DEFAULT_ROLES.keys + invalid_keys = roles.keys - valid_keys + + if invalid_keys.any? + return "#{message}: value at index #{index} for roles key must be in #{valid_keys} but was #{invalid_keys}" + end + + fields.roles = roles + end + + nil + end + + def validate_message_type(wamp_msg, index, fields, message) + error = validate_int(wamp_msg[index], index, message) + + return error if error + + fields.message_type = wamp_msg[index] + nil + end + + def validate_signature(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.signature = wamp_msg[index] + nil + end + + def validate_reason(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.reason = wamp_msg[index] + nil + end + + def validate_topic(wamp_msg, index, fields, message) + error = validate_string(wamp_msg[index], index, message) + + return error if error + + fields.topic = wamp_msg[index] + nil + end + + def validate_extra(wamp_msg, index, fields, message) + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + fields.extra = wamp_msg[index] + nil + end + + def validate_options(wamp_msg, index, fields, message) + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + fields.options = wamp_msg[index] + nil + end + + def validate_details(wamp_msg, index, fields, message) + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + fields.details = wamp_msg[index] + nil + end + + def validate_subscription_id(wamp_msg, index, fields, message) + error = validate_id(wamp_msg[index], index, message) + + return error if error + + fields.subscription_id = wamp_msg[index] + nil + end + + def validate_publication_id(wamp_msg, index, fields, message) + error = validate_id(wamp_msg[index], index, message) + + return error if error + + fields.publication_id = wamp_msg[index] + nil + end + + def validate_registration_id(wamp_msg, index, fields, message) + error = validate_id(wamp_msg[index], index, message) + + return error if error + + fields.registration_id = wamp_msg[index] + nil + end + + def validate_hello_details(wamp_msg, index, fields, message) # rubocop: disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength + errors = [] + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + error = validate_authid(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authrole(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authmethods(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_roles(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authextra(wamp_msg[index], index, fields, message) + errors.append(error) if error + + return errors unless errors.empty? + + fields.details = wamp_msg[index] + nil + end + + def validate_welcome_details(wamp_msg, index, fields, message) # rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength + errors = [] + error = validate_hash(wamp_msg[index], index, message) + + return error if error + + error = validate_roles(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authid(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authrole(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_welcome_authmethod(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_roles(wamp_msg[index], index, fields, message) + errors.append(error) if error + + error = validate_authextra(wamp_msg[index], index, fields, message) + errors.append(error) if error + + return errors unless errors.empty? + + fields.details = wamp_msg[index] + nil + end + + def sanity_check(wamp_message, min_length, max_length, expected_id, message) # rubocop:disable Metrics/MethodLength + unless wamp_message.is_a?(Array) + raise ArgumentError, "invalid message type #{wamp_message.class} for #{message}, type should be a list" + end + + if wamp_message.length < min_length + raise ArgumentError, "invalid message length #{wamp_message.length}, must be at least #{min_length}" + end + + if wamp_message.length > max_length + raise ArgumentError, "invalid message length #{wamp_message.length}, must be at most #{min_length}" + end + + message_id = wamp_message[0] + return if message_id == expected_id + + raise ArgumentError, "invalid message id #{message_id} for #{message}, expected #{expected_id}" + end + + def validate_message(wamp_msg, type, val_spec) + sanity_check(wamp_msg, val_spec.min_length, val_spec.max_length, type, val_spec.message) + errors = [] + fields = Fields.new + val_spec.spec.each do |idx, func| + error = func.call(wamp_msg, idx, fields, val_spec.message) + errors.append(error) if error + end + raise ArgumentError, errors.join(", ") unless errors.empty? + + fields + end + end + end +end diff --git a/lib/wampproto/message/validation_spec.rb b/lib/wampproto/message/validation_spec.rb new file mode 100644 index 0000000..0b43483 --- /dev/null +++ b/lib/wampproto/message/validation_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Wampproto + module Message + # validation spec + class ValidationSpec + attr_reader :min_length, :max_length, :message, :spec + + def initialize(min_length, max_length, message, spec) + @min_length = min_length + @max_length = max_length + @message = message + @spec = spec + end + end + end +end diff --git a/sig/wampproto/message/abort.rbs b/sig/wampproto/message/abort.rbs index 2c08d4a..59f5b14 100644 --- a/sig/wampproto/message/abort.rbs +++ b/sig/wampproto/message/abort.rbs @@ -1,7 +1,46 @@ module Wampproto module Message + class IAbortFields + def details: () -> Hash[Symbol, untyped] + + def reason: () -> String + + def args: () -> (Array[untyped] | nil) + + def kwargs: () -> (Hash[Symbol, untyped] | nil) + end + + class AbortFields < IAbortFields + + @details: Hash[Symbol, untyped] + + @reason: String + + @args: Array[untyped] + + @kwargs: Hash[Symbol, untyped] + + @marshal: Array[untyped] + + attr_reader details: Hash[Symbol, untyped] + + attr_reader reason: String + + attr_reader args: Array[untyped] + + attr_reader kwargs: Hash[Symbol, untyped] + + def initialize: (Hash[Symbol, untyped] details, String reason, *Array[untyped] args, **Hash[Symbol, untyped] kwargs) -> void + + end + # abort message class Abort < Base + + TEXT: String + + VALIDATION_SPEC: ValidationSpec + @details: Hash[Symbol, untyped] @reason: String @@ -12,6 +51,8 @@ module Wampproto @marshal: Array[untyped] + @fields: IAbortFields + attr_reader details: Hash[Symbol, untyped] attr_reader reason: String @@ -22,10 +63,11 @@ module Wampproto def initialize: (Hash[Symbol, untyped] details, String reason, *Array[untyped] args, **Hash[Symbol, untyped] kwargs) -> void + def self.with_fields: (IAbortFields fields) -> Abort + def marshal: () -> Array[untyped] def self.parse: (Array[untyped] wamp_message) -> Abort end end end - diff --git a/sig/wampproto/message/exceptions.rbs b/sig/wampproto/message/exceptions.rbs new file mode 100644 index 0000000..cbe1993 --- /dev/null +++ b/sig/wampproto/message/exceptions.rbs @@ -0,0 +1,9 @@ +module Wampproto + module Message + module Exceptions + INVALID_DATA_TYPE_ERROR: String + INVALID_RANGE_ERROR: String + INVALID_DETAIL_ERROR: String + end + end +end diff --git a/sig/wampproto/message/util.rbs b/sig/wampproto/message/util.rbs new file mode 100644 index 0000000..04fddb7 --- /dev/null +++ b/sig/wampproto/message/util.rbs @@ -0,0 +1,106 @@ +module Wampproto + module Message + class Fields + + attr_accessor request_id: Integer | nil + attr_accessor uri: String | nil + attr_accessor args: Array[untyped] | nil + attr_accessor kwargs: Hash[Symbol, untyped] | nil + + attr_accessor session_id: Integer | nil + + attr_accessor realm: String | nil + attr_accessor authid: String | nil + attr_accessor authrole: String | nil + attr_accessor authmethod: String | nil + attr_accessor authmethods: Array[String] | nil + attr_accessor authextra: Hash[Symbol, untyped] | nil + attr_accessor roles: Hash[Symbol, untyped] | nil + + attr_accessor message_type: Integer | nil + attr_accessor signature: String | nil + attr_accessor reason: String | nil + attr_accessor topic: String | nil + + attr_accessor extra: Hash[Symbol, untyped] | nil + attr_accessor options: Hash[Symbol, untyped] | nil + attr_accessor details: Hash[Symbol, untyped] | nil + + attr_accessor subscription_id: Integer | nil + attr_accessor publication_id: Integer | nil + + attr_accessor registration_id: Integer | nil + end + + module Util + MIN_ID: Integer + MAX_ID: Integer + DEFAULT_ROLES: Hash[Symbol, Hash[untyped, untyped]] + + def validate_int: (Integer value, Integer index, String message) -> (String | nil) + + def validate_string: (String value, Integer index, String message) -> (String | nil) + + def validate_list: (Array[untyped] value, Integer index, String message) -> (String | nil) + + def validate_hash: (Hash[String, untyped] value, Integer index, String message) -> (String | nil) + + def validate_id: (Integer value, Integer index, String message) -> (String | nil) + + def validate_request_id: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_uri: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_args: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_kwargs: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_session_id: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_realm: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_authid: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_authrole: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_authmethod: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_authmethods: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_welcome_authmethod: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_authextra: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_roles: (Hash[String, untyped] details, Integer index, Fields fields, String message) -> (String | nil) + + def validate_message_type: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_signature: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_reason: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_topic: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_extra: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_options: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_details: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_hello_details: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | Array[untyped] | nil) + + def validate_welcome_details: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | Array[untyped] | nil) + + def validate_subscription_id: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_publication_id: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def validate_registration_id: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | nil) + + def sanity_check: (Array[untyped] wamp_msg, Integer min_length, Integer max_length, Integer expected_id, String message) -> void + + def self.validate_message: (Array[untyped] wamp_msg, Integer type, ValidationSpec val_spec) -> Fields + + end + end +end diff --git a/sig/wampproto/message/validation_spec.rbs b/sig/wampproto/message/validation_spec.rbs new file mode 100644 index 0000000..02072e4 --- /dev/null +++ b/sig/wampproto/message/validation_spec.rbs @@ -0,0 +1,25 @@ +module Wampproto + module Message + class ValidationSpec + @min_length: Integer + + @max_length: Integer + + @message: String + + @spec: Hash[Integer, Method] + + attr_reader min_length: Integer + + attr_reader max_length: Integer + + attr_reader message: String + + attr_reader spec: Hash[Integer, Method] + + def initialize: (Integer min_length, Integer max_length, String message, Hash[Integer, Method] spec) -> void + + end + end +end +