lib/simple/service/action.rb in simple-service-0.1.1 vs lib/simple/service/action.rb in simple-service-0.1.2

- old
+ new

@@ -1,41 +1,48 @@ -# rubocop:disable Metrics/CyclomaticComplexity -# rubocop:disable Metrics/AbcSize -# rubocop:disable Metrics/MethodLength -# rubocop:disable Metrics/PerceivedComplexity - module Simple::Service class Action end end require_relative "./action/comment" require_relative "./action/parameter" module Simple::Service + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Style/GuardClause + # rubocop:disable Metrics/ClassLength + class Action - ArgumentError = ::Simple::Service::ArgumentError + IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # :nodoc: + IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # :nodoc: - IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" - IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") - # determines all services provided by the +service+ service module. def self.enumerate(service:) # :nodoc: service.public_instance_methods(false) .grep(IDENTIFIER_REGEXP) .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) } end attr_reader :service attr_reader :name + def full_name + "#{service.name}##{name}" + end + + def to_s # :nodoc: + full_name + end + # returns an Array of Parameter structures. def parameters @parameters ||= Parameter.reflect_on_method(service: service, name: name) end - def initialize(service, name) + def initialize(service, name) # :nodoc: @service = service @name = name parameters end @@ -63,131 +70,125 @@ @service.instance_method(name).source_location end # build a service_instance and run the action, with arguments constructed from # args_hsh and params_hsh. - def invoke(args, options) - args ||= {} - options ||= {} - + def invoke(*args, **named_args) # convert Array arguments into a Hash of named arguments. This is strictly # necessary to be able to apply default value-based type conversions. (On # the downside this also means we convert an array to a hash and then back # into an array. This, however, should only be an issue for CLI based action # invocations, because any other use case (that I can think of) should allow - # us to provide arguments as a Hash. - if args.is_a?(Array) - args = convert_argument_array_to_hash(args) - end + # us to provide arguments as a Hash. + args = convert_argument_array_to_hash(args) + named_args = named_args.merge(args) - # [TODO] Type conversion according to default values. - args_ary = build_method_arguments(args, options) + invoke2(args: named_args, flags: {}) + end + # invokes an action with a given +name+ in a service with a Hash of arguments. + # + # You cannot call this method if the context is not set. + def invoke2(args:, flags:) + verify_required_args!(args, flags) + + positionals = build_positional_arguments(args, flags) + keywords = build_keyword_arguments(args.merge(flags)) + service_instance = Object.new service_instance.extend service - service_instance.public_send(@name, *args_ary) + + if keywords.empty? + service_instance.public_send(@name, *positionals) + else + # calling this with an empty keywords Hash still raises an ArgumentError + # if the target method does not accept arguments. + service_instance.public_send(@name, *positionals, **keywords) + end end private - module IndifferentHashEx - def self.fetch(hsh, name) - missing_key!(name) unless hsh + # returns an error if the keywords hash does not define all required keyword arguments. + def verify_required_args!(args, flags) # :nodoc: + @required_names ||= parameters.select(&:required?).map(&:name) - hsh.fetch(name.to_sym) do - hsh.fetch(name.to_s) do - missing_key!(name) - end - end - end + missing_parameters = @required_names - args.keys - flags.keys + return if missing_parameters.empty? - def self.key?(hsh, name) - return false unless hsh + raise ::Simple::Service::MissingArguments.new(self, missing_parameters) + end - hsh.key?(name.to_sym) || hsh.key?(name.to_s) - end + # Enumerating all parameters it puts all named parameters into a Hash + # of keyword arguments. + def build_keyword_arguments(args) + @keyword_names ||= parameters.select(&:keyword?).map(&:name) - def self.missing_key!(name) - raise ArgumentError, "Missing argument in arguments hash: #{name}" - end + keys = @keyword_names & args.keys + values = args.fetch_values(*keys) + + Hash[keys.zip(values)] end - I = IndifferentHashEx + def variadic_parameter + return @variadic_parameter if defined? @variadic_parameter - # returns an array of arguments suitable to be sent to the action method. - def build_method_arguments(args_hsh, params_hsh) - args = [] - keyword_args = {} + @variadic_parameter = parameters.detect(&:variadic?) + end - parameters.each do |parameter| - if parameter.keyword? - if I.key?(params_hsh, parameter.name) - keyword_args[parameter.name] = I.fetch(params_hsh, parameter.name) - end - else - if parameter.variadic? - if I.key?(args_hsh, parameter.name) - args.concat(Array(I.fetch(args_hsh, parameter.name))) - end - else - if !parameter.optional? || I.key?(args_hsh, parameter.name) - args << I.fetch(args_hsh, parameter.name) - end - end + def positional_names + @positional_names ||= parameters.select(&:positional?).map(&:name) + end + + # Enumerating all parameters it collects all positional parameters into + # an Array. + def build_positional_arguments(args, flags) + positionals = positional_names.each_with_object([]) do |parameter_name, ary| + if args.key?(parameter_name) + ary << args[parameter_name] + elsif flags.key?(parameter_name) + ary << flags[parameter_name] end end - unless keyword_args.empty? - args << keyword_args + # A variadic parameter is appended to the positionals array. + # It is always optional - but if it exists it must be an Array. + if variadic_parameter + value = if args.key?(variadic_parameter.name) + args[variadic_parameter.name] + elsif flags.key?(variadic_parameter.name) + flags[variadic_parameter.name] + end + + positionals.concat(value) if value end - args + positionals end def convert_argument_array_to_hash(ary) - # enumerate all of the action's anonymous arguments, trying to match them - # against the values in +ary+. If afterwards any arguments are still left - # in +ary+ they will be assigned to the variadic arguments array, which - # - if a variadic parameter is defined in this action - will be added to - # the hash as well. + expect! ary => Array + hsh = {} - variadic_parameter_name = nil - parameters.each do |parameter| - next if parameter.keyword? - parameter_name = parameter.name + if variadic_parameter + hsh[variadic_parameter.name] = [] + end - if parameter.variadic? - variadic_parameter_name = parameter_name - next - end + if ary.length > positional_names.length + extra_arguments = ary[positional_names.length..-1] - if ary.empty? && !parameter.optional? - raise ::Simple::Service::ArgumentError, "Missing #{parameter_name} parameter" + if variadic_parameter + hsh[variadic_parameter.name] = extra_arguments + else + raise ::Simple::Service::ExtraArguments.new(self, extra_arguments) end - - next if ary.empty? - - hsh[parameter_name] = ary.shift end - # Any arguments are left? Set variadic parameter, if defined, raise an error otherwise. - unless ary.empty? - unless variadic_parameter_name - raise ::Simple::Service::ArgumentError, "Extra parameters: #{ary.map(&:inspect).join(", ")}" - end - - hsh[variadic_parameter_name] = ary + ary.zip(positional_names).each do |value, parameter_name| + hsh[parameter_name] = value end hsh - end - - def full_name - "#{service}##{name}" - end - - def to_s - full_name end end end