diff --git a/lib/wampproto/message.rb b/lib/wampproto/message.rb index a9f2cfe..f2227df 100644 --- a/lib/wampproto/message.rb +++ b/lib/wampproto/message.rb @@ -30,6 +30,8 @@ require_relative "message/invocation" require_relative "message/yield" +require_relative "message/util" + module Wampproto # message root module Message diff --git a/lib/wampproto/message/abort.rb b/lib/wampproto/message/abort.rb index 460f54c..389a65c 100644 --- a/lib/wampproto/message/abort.rb +++ b/lib/wampproto/message/abort.rb @@ -2,10 +2,41 @@ module Wampproto module Message + # interface for abort fields + class IAbortFields + attr_reader :details, :reason, :args, :kwargs + 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 = 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) @@ -14,6 +45,10 @@ def initialize(details, reason, *args, **kwargs) @kwargs = Validate.hash!("Keyword Arguments", kwargs) end + def self.with_fields(fields) + new(fields.details, fields.reason, *fields.args, **fields.kwargs) + end + def marshal @marshal = [Type::ABORT, details, reason] @marshal << args if kwargs.any? || args.any? diff --git a/lib/wampproto/message/exceptions.rb b/lib/wampproto/message/exceptions.rb new file mode 100644 index 0000000..4f31149 --- /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..2780b09 --- /dev/null +++ b/lib/wampproto/message/util.rb @@ -0,0 +1,476 @@ +# 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 + + def initialize # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + @request_id = nil + @uri = nil + @args = nil + @kwargs = nil + + @session_id = nil + + @realm = nil + @authid = nil + @authrole = nil + @authmethod = nil + @authmethods = nil + @authextra = nil + @roles = nil + + @message_type = nil + @signature = nil + @reason = nil + @topic = nil + + @extra = nil + @options = nil + @details = nil + + @subscription_id = nil + @publication_id = nil + @registration_id = nil + end + end + + # util module + module Util # rubocop:disable Metrics/ModuleLength + 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_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: value.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(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authrole", + expected_type: "string", + actual_type: value.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(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authmethods", + expected_type: "list", + actual_type: value.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(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authmethod", + expected_type: "string", + actual_type: value.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(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "authextra", + expected_type: "hash", + actual_type: value.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(authid, index, message) + new_error = format( + Exceptions::INVALID_DETAIL_ERROR, + message:, + index:, + key: "roles", + expected_type: "hash", + actual_type: value.class + ) + + return new_error if error + + valid_keys = default_roles.keys + invalid_keys = hash.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 value.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, message, val_spec) + sanity_check(wamp_msg, val_spec.min_length, val_spec.max_length, type, message) + errors = [] + f = Fields.new + val_spec.spec.each do |idx, func| + error = send(func, message, idx, f, val_spec.message) + errors.append(error) if error + end + raise ArgumentError, *errors unless errors.empty? + + f + 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..a95eb31 100644 --- a/sig/wampproto/message/abort.rbs +++ b/sig/wampproto/message/abort.rbs @@ -1,7 +1,31 @@ module Wampproto module Message + class 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] + end + # abort message class Abort < Base + TEXT: String + + VALIDATION_SPEC: ValidationSpec + @details: Hash[Symbol, untyped] @reason: String @@ -22,6 +46,8 @@ module Wampproto def initialize: (Hash[Symbol, untyped] details, String reason, *Array[untyped] args, **Hash[Symbol, untyped] kwargs) -> void + def self.with_fields: (IAbortFields fields) -> void + def marshal: () -> Array[untyped] def self.parse: (Array[untyped] wamp_message) -> Abort 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..27f16dc --- /dev/null +++ b/sig/wampproto/message/util.rbs @@ -0,0 +1,102 @@ +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[String, 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[String, untyped] | nil + attr_accessor roles: Hash[String, 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[String, untyped] | nil + attr_accessor options: Hash[String, untyped] | nil + attr_accessor details: Hash[String, untyped] | nil + + attr_accessor subscription_id: Integer | nil + attr_accessor publication_id: Integer | nil + + attr_accessor registration_id: Integer | nil + end + + module Util + def validate_int: (Integer value, Integer index, String message) -> (String | Integer) + + 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 | nil) + + def validate_welcome_details: (Array[untyped] wamp_msg, Integer index, Fields fields, String message) -> (String | 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 validate_message: (Array[untyped] wamp_msg, Integer type, String message, 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 +