simple-service 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.gitignore +12 -0
 - data/.rubocop.yml +100 -0
 - data/.tm_properties +1 -0
 - data/Gemfile +14 -0
 - data/Makefile +16 -0
 - data/README.md +3 -0
 - data/Rakefile +6 -0
 - data/VERSION +1 -0
 - data/bin/bundle +105 -0
 - data/bin/console +15 -0
 - data/bin/rake +29 -0
 - data/bin/rspec +29 -0
 - data/lib/simple-service.rb +3 -0
 - data/lib/simple/service.rb +103 -0
 - data/lib/simple/service/action.rb +203 -0
 - data/lib/simple/service/action/comment.rb +57 -0
 - data/lib/simple/service/action/indie_hash.rb +37 -0
 - data/lib/simple/service/action/method_reflection.rb +70 -0
 - data/lib/simple/service/action/parameter.rb +42 -0
 - data/lib/simple/service/context.rb +94 -0
 - data/lib/simple/service/errors.rb +54 -0
 - data/lib/simple/service/version.rb +29 -0
 - data/log/.gitkeep +0 -0
 - data/scripts/release +2 -0
 - data/scripts/release.rb +91 -0
 - data/scripts/stats +5 -0
 - data/scripts/watch +2 -0
 - data/simple-service.gemspec +25 -0
 - data/spec/simple/service/action_invoke2_spec.rb +166 -0
 - data/spec/simple/service/action_invoke_spec.rb +266 -0
 - data/spec/simple/service/action_spec.rb +51 -0
 - data/spec/simple/service/context_spec.rb +69 -0
 - data/spec/simple/service/service_spec.rb +105 -0
 - data/spec/simple/service/version_spec.rb +7 -0
 - data/spec/spec_helper.rb +38 -0
 - data/spec/support/spec_services.rb +50 -0
 - metadata +99 -0
 
| 
         @@ -0,0 +1,203 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Simple::Service
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Action
         
     | 
| 
      
 3 
     | 
    
         
            +
              end
         
     | 
| 
      
 4 
     | 
    
         
            +
            end
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "./action/comment"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require_relative "./action/parameter"
         
     | 
| 
      
 8 
     | 
    
         
            +
            require_relative "./action/indie_hash"
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            module Simple::Service
         
     | 
| 
      
 11 
     | 
    
         
            +
              # rubocop:disable Metrics/AbcSize
         
     | 
| 
      
 12 
     | 
    
         
            +
              # rubocop:disable Metrics/PerceivedComplexity
         
     | 
| 
      
 13 
     | 
    
         
            +
              # rubocop:disable Metrics/CyclomaticComplexity
         
     | 
| 
      
 14 
     | 
    
         
            +
              # rubocop:disable Style/GuardClause
         
     | 
| 
      
 15 
     | 
    
         
            +
              # rubocop:disable Metrics/ClassLength
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              class Action
         
     | 
| 
      
 18 
     | 
    
         
            +
                IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # :nodoc:
         
     | 
| 
      
 19 
     | 
    
         
            +
                IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # :nodoc:
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                # determines all services provided by the +service+ service module.
         
     | 
| 
      
 22 
     | 
    
         
            +
                def self.enumerate(service:) # :nodoc:
         
     | 
| 
      
 23 
     | 
    
         
            +
                  service.public_instance_methods(false)
         
     | 
| 
      
 24 
     | 
    
         
            +
                         .grep(IDENTIFIER_REGEXP)
         
     | 
| 
      
 25 
     | 
    
         
            +
                         .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                attr_reader :service
         
     | 
| 
      
 29 
     | 
    
         
            +
                attr_reader :name
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def full_name
         
     | 
| 
      
 32 
     | 
    
         
            +
                  "#{service.name}##{name}"
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def to_s # :nodoc:
         
     | 
| 
      
 36 
     | 
    
         
            +
                  full_name
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # returns an Array of Parameter structures.
         
     | 
| 
      
 40 
     | 
    
         
            +
                def parameters
         
     | 
| 
      
 41 
     | 
    
         
            +
                  @parameters ||= Parameter.reflect_on_method(service: service, name: name)
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def initialize(service, name) # :nodoc:
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @service  = service
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @name     = name
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  parameters
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                def short_description
         
     | 
| 
      
 52 
     | 
    
         
            +
                  comment.short
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def full_description
         
     | 
| 
      
 56 
     | 
    
         
            +
                  comment.full
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                private
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                # returns a Comment object
         
     | 
| 
      
 62 
     | 
    
         
            +
                #
         
     | 
| 
      
 63 
     | 
    
         
            +
                # The comment object is extracted on demand on the first call.
         
     | 
| 
      
 64 
     | 
    
         
            +
                def comment
         
     | 
| 
      
 65 
     | 
    
         
            +
                  @comment ||= Comment.extract(action: self)
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                public
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                def source_location
         
     | 
| 
      
 71 
     | 
    
         
            +
                  @service.instance_method(name).source_location
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                # build a service_instance and run the action, with arguments constructed from
         
     | 
| 
      
 75 
     | 
    
         
            +
                # args_hsh and params_hsh.
         
     | 
| 
      
 76 
     | 
    
         
            +
                def invoke(*args, **named_args)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  # convert Array arguments into a Hash of named arguments. This is strictly
         
     | 
| 
      
 78 
     | 
    
         
            +
                  # necessary to be able to apply default value-based type conversions. (On
         
     | 
| 
      
 79 
     | 
    
         
            +
                  # the downside this also means we convert an array to a hash and then back
         
     | 
| 
      
 80 
     | 
    
         
            +
                  # into an array. This, however, should only be an issue for CLI based action
         
     | 
| 
      
 81 
     | 
    
         
            +
                  # invocations, because any other use case (that I can think of) should allow
         
     | 
| 
      
 82 
     | 
    
         
            +
                  # us to provide arguments as a Hash.
         
     | 
| 
      
 83 
     | 
    
         
            +
                  args = convert_argument_array_to_hash(args)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  named_args = named_args.merge(args)
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  invoke2(args: named_args, flags: {})
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                # invokes an action with a given +name+ in a service with a Hash of arguments.
         
     | 
| 
      
 90 
     | 
    
         
            +
                #
         
     | 
| 
      
 91 
     | 
    
         
            +
                # You cannot call this method if the context is not set.
         
     | 
| 
      
 92 
     | 
    
         
            +
                def invoke2(args:, flags:)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  # args and flags are being stringified. This is necessary to not allow any
         
     | 
| 
      
 94 
     | 
    
         
            +
                  # unchecked input to DOS this process by just providing always changing
         
     | 
| 
      
 95 
     | 
    
         
            +
                  # key values.
         
     | 
| 
      
 96 
     | 
    
         
            +
                  args = IndieHash.new(args)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  flags = IndieHash.new(flags)
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                  verify_required_args!(args, flags)
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                  positionals = build_positional_arguments(args, flags)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  keywords = build_keyword_arguments(args.merge(flags))
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                  service_instance = Object.new
         
     | 
| 
      
 105 
     | 
    
         
            +
                  service_instance.extend service
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                  if keywords.empty?
         
     | 
| 
      
 108 
     | 
    
         
            +
                    service_instance.public_send(@name, *positionals)
         
     | 
| 
      
 109 
     | 
    
         
            +
                  else
         
     | 
| 
      
 110 
     | 
    
         
            +
                    # calling this with an empty keywords Hash still raises an ArgumentError
         
     | 
| 
      
 111 
     | 
    
         
            +
                    # if the target method does not accept arguments.
         
     | 
| 
      
 112 
     | 
    
         
            +
                    service_instance.public_send(@name, *positionals, **keywords)
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
      
 114 
     | 
    
         
            +
                end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                private
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                # returns an error if the keywords hash does not define all required keyword arguments.
         
     | 
| 
      
 119 
     | 
    
         
            +
                def verify_required_args!(args, flags) # :nodoc:
         
     | 
| 
      
 120 
     | 
    
         
            +
                  @required_names ||= parameters.select(&:required?).map(&:name).map(&:to_s)
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                  missing_parameters = @required_names - args.keys - flags.keys
         
     | 
| 
      
 123 
     | 
    
         
            +
                  return if missing_parameters.empty?
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                  raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
         
     | 
| 
      
 126 
     | 
    
         
            +
                end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                # Enumerating all parameters it puts all named parameters into a Hash
         
     | 
| 
      
 129 
     | 
    
         
            +
                # of keyword arguments.
         
     | 
| 
      
 130 
     | 
    
         
            +
                def build_keyword_arguments(args)
         
     | 
| 
      
 131 
     | 
    
         
            +
                  @keyword_names ||= parameters.select(&:keyword?).map(&:name).map(&:to_s)
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                  keys = @keyword_names & args.keys
         
     | 
| 
      
 134 
     | 
    
         
            +
                  values = args.fetch_values(*keys)
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                  # Note that +keys+ now only contains names of keyword arguments that actually exist.
         
     | 
| 
      
 137 
     | 
    
         
            +
                  # This is therefore not a way to DOS this process.
         
     | 
| 
      
 138 
     | 
    
         
            +
                  Hash[keys.map(&:to_sym).zip(values)]
         
     | 
| 
      
 139 
     | 
    
         
            +
                end
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                def variadic_parameter
         
     | 
| 
      
 142 
     | 
    
         
            +
                  return @variadic_parameter if defined? @variadic_parameter
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                  @variadic_parameter = parameters.detect(&:variadic?)
         
     | 
| 
      
 145 
     | 
    
         
            +
                end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                def positional_names
         
     | 
| 
      
 148 
     | 
    
         
            +
                  @positional_names ||= parameters.select(&:positional?).map(&:name)
         
     | 
| 
      
 149 
     | 
    
         
            +
                end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                # Enumerating all parameters it collects all positional parameters into
         
     | 
| 
      
 152 
     | 
    
         
            +
                # an Array.
         
     | 
| 
      
 153 
     | 
    
         
            +
                def build_positional_arguments(args, flags)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  positionals = positional_names.each_with_object([]) do |parameter_name, ary|
         
     | 
| 
      
 155 
     | 
    
         
            +
                    if args.key?(parameter_name)
         
     | 
| 
      
 156 
     | 
    
         
            +
                      ary << args[parameter_name]
         
     | 
| 
      
 157 
     | 
    
         
            +
                    elsif flags.key?(parameter_name)
         
     | 
| 
      
 158 
     | 
    
         
            +
                      ary << flags[parameter_name]
         
     | 
| 
      
 159 
     | 
    
         
            +
                    end
         
     | 
| 
      
 160 
     | 
    
         
            +
                  end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                  # A variadic parameter is appended to the positionals array.
         
     | 
| 
      
 163 
     | 
    
         
            +
                  # It is always optional - but if it exists it must be an Array.
         
     | 
| 
      
 164 
     | 
    
         
            +
                  if variadic_parameter
         
     | 
| 
      
 165 
     | 
    
         
            +
                    value = if args.key?(variadic_parameter.name)
         
     | 
| 
      
 166 
     | 
    
         
            +
                              args[variadic_parameter.name]
         
     | 
| 
      
 167 
     | 
    
         
            +
                            elsif flags.key?(variadic_parameter.name)
         
     | 
| 
      
 168 
     | 
    
         
            +
                              flags[variadic_parameter.name]
         
     | 
| 
      
 169 
     | 
    
         
            +
                            end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                    positionals.concat(value) if value
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                  positionals
         
     | 
| 
      
 175 
     | 
    
         
            +
                end
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                def convert_argument_array_to_hash(ary)
         
     | 
| 
      
 178 
     | 
    
         
            +
                  expect! ary => Array
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                  hsh = {}
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
                  if variadic_parameter
         
     | 
| 
      
 183 
     | 
    
         
            +
                    hsh[variadic_parameter.name] = []
         
     | 
| 
      
 184 
     | 
    
         
            +
                  end
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
                  if ary.length > positional_names.length
         
     | 
| 
      
 187 
     | 
    
         
            +
                    extra_arguments = ary[positional_names.length..-1]
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
                    if variadic_parameter
         
     | 
| 
      
 190 
     | 
    
         
            +
                      hsh[variadic_parameter.name] = extra_arguments
         
     | 
| 
      
 191 
     | 
    
         
            +
                    else
         
     | 
| 
      
 192 
     | 
    
         
            +
                      raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
         
     | 
| 
      
 193 
     | 
    
         
            +
                    end
         
     | 
| 
      
 194 
     | 
    
         
            +
                  end
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                  ary.zip(positional_names).each do |value, parameter_name|
         
     | 
| 
      
 197 
     | 
    
         
            +
                    hsh[parameter_name] = value
         
     | 
| 
      
 198 
     | 
    
         
            +
                  end
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                  hsh
         
     | 
| 
      
 201 
     | 
    
         
            +
                end
         
     | 
| 
      
 202 
     | 
    
         
            +
              end
         
     | 
| 
      
 203 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,57 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # returns the comment for an action
         
     | 
| 
      
 2 
     | 
    
         
            +
            class ::Simple::Service::Action::Comment # :nodoc:
         
     | 
| 
      
 3 
     | 
    
         
            +
              attr_reader :short
         
     | 
| 
      
 4 
     | 
    
         
            +
              attr_reader :full
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def self.extract(action:)
         
     | 
| 
      
 7 
     | 
    
         
            +
                file, line = action.source_location
         
     | 
| 
      
 8 
     | 
    
         
            +
                lines = Extractor.extract_comment_lines(file: file, before_line: line)
         
     | 
| 
      
 9 
     | 
    
         
            +
                full = lines[2..-1].join("\n") if lines.length >= 2
         
     | 
| 
      
 10 
     | 
    
         
            +
                new short: lines[0], full: full
         
     | 
| 
      
 11 
     | 
    
         
            +
              end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def initialize(short:, full:)
         
     | 
| 
      
 14 
     | 
    
         
            +
                @short, @full = short, full
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              module Extractor
         
     | 
| 
      
 18 
     | 
    
         
            +
                extend self
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                # reads the source \a file and turns each non-comment into :code and each comment
         
     | 
| 
      
 21 
     | 
    
         
            +
                # into a string without the leading comment markup.
         
     | 
| 
      
 22 
     | 
    
         
            +
                def parse_source(file)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @parsed_sources ||= {}
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @parsed_sources[file] = _parse_source(file)
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def _parse_source(file)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  File.readlines(file).map do |line|
         
     | 
| 
      
 29 
     | 
    
         
            +
                    case line
         
     | 
| 
      
 30 
     | 
    
         
            +
                    when /^\s*# ?(.*)$/ then $1
         
     | 
| 
      
 31 
     | 
    
         
            +
                    when /^\s*end/ then :end
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def extract_comment_lines(file:, before_line:)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  parsed_source = parse_source(file)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  # go down from before_line until we see a line which is either a comment
         
     | 
| 
      
 40 
     | 
    
         
            +
                  # or an :end. Note that the line at before_line-1 should be the first
         
     | 
| 
      
 41 
     | 
    
         
            +
                  # line of the method definition in question.
         
     | 
| 
      
 42 
     | 
    
         
            +
                  last_line = before_line - 1
         
     | 
| 
      
 43 
     | 
    
         
            +
                  last_line -= 1 while last_line >= 0 && !parsed_source[last_line]
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  first_line = last_line
         
     | 
| 
      
 46 
     | 
    
         
            +
                  first_line -= 1 while first_line >= 0 && parsed_source[first_line]
         
     | 
| 
      
 47 
     | 
    
         
            +
                  first_line += 1
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  comments = parsed_source[first_line..last_line]
         
     | 
| 
      
 50 
     | 
    
         
            +
                  if comments.include?(:end)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    []
         
     | 
| 
      
 52 
     | 
    
         
            +
                  else
         
     | 
| 
      
 53 
     | 
    
         
            +
                    parsed_source[first_line..last_line]
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,37 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Simple::Service::Action
         
     | 
| 
      
 2 
     | 
    
         
            +
              # The IndieHash class defines as much of the Hash interface as necessary for simple-service
         
     | 
| 
      
 3 
     | 
    
         
            +
              # to successfully run.
         
     | 
| 
      
 4 
     | 
    
         
            +
              class IndieHash
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(hsh)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @hsh = hsh.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def keys
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @hsh.keys
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def fetch_values(*keys)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  keys = keys.map(&:to_s)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @hsh.fetch_values(*keys)
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def key?(sym)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @hsh.key?(sym.to_s)
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def [](sym)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @hsh[sym.to_s]
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def merge(other_hsh)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @hsh = @hsh.merge(other_hsh.send(:__hsh__))
         
     | 
| 
      
 28 
     | 
    
         
            +
                  self
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                private
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def __hsh__
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @hsh
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,70 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # rubocop:disable Metrics/AbcSize
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module ::Simple::Service::Action::MethodReflection # :nodoc:
         
     | 
| 
      
 4 
     | 
    
         
            +
              extend self
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              #
         
     | 
| 
      
 7 
     | 
    
         
            +
              # returns an array with entries like the following:
         
     | 
| 
      
 8 
     | 
    
         
            +
              #
         
     | 
| 
      
 9 
     | 
    
         
            +
              #  [ :key, name, default_value ]
         
     | 
| 
      
 10 
     | 
    
         
            +
              #  [ :keyreq, name [, nil ] ]
         
     | 
| 
      
 11 
     | 
    
         
            +
              #  [ :req, name [, nil ] ]
         
     | 
| 
      
 12 
     | 
    
         
            +
              #  [ :opt, name [, nil ] ]
         
     | 
| 
      
 13 
     | 
    
         
            +
              #  [ :rest, name [, nil ] ]
         
     | 
| 
      
 14 
     | 
    
         
            +
              #
         
     | 
| 
      
 15 
     | 
    
         
            +
              def parameters(service, method_id)
         
     | 
| 
      
 16 
     | 
    
         
            +
                method = service.instance_method(method_id)
         
     | 
| 
      
 17 
     | 
    
         
            +
                parameters = method.parameters
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # method parameters with a :key mode are optional named arguments. We only
         
     | 
| 
      
 20 
     | 
    
         
            +
                # support defaults for those - if there are none we abort here already.
         
     | 
| 
      
 21 
     | 
    
         
            +
                keys = parameters.map { |mode, name| name if mode == :key }.compact
         
     | 
| 
      
 22 
     | 
    
         
            +
                return parameters if keys.empty?
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # We are now doing a fake call to the method, with a minimal viable set of
         
     | 
| 
      
 25 
     | 
    
         
            +
                # arguments, to let the ruby runtime fill in default values for arguments.
         
     | 
| 
      
 26 
     | 
    
         
            +
                # We do not, however, let the call complete. Instead we use a TracePoint to
         
     | 
| 
      
 27 
     | 
    
         
            +
                # abort as soon as the method is called, and use the its binding to determine
         
     | 
| 
      
 28 
     | 
    
         
            +
                # the default values.
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                fake_recipient = Object.new.extend(service)
         
     | 
| 
      
 31 
     | 
    
         
            +
                fake_call_args = minimal_arguments(method)
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                trace_point = TracePoint.trace(:call) do |tp|
         
     | 
| 
      
 34 
     | 
    
         
            +
                  throw :received_fake_call, tp.binding if tp.defined_class == service && tp.method_id == method_id
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                bnd = catch(:received_fake_call) do
         
     | 
| 
      
 38 
     | 
    
         
            +
                  fake_recipient.send(method_id, *fake_call_args)
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                trace_point.disable
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                # extract default values from the received binding, and merge with the
         
     | 
| 
      
 44 
     | 
    
         
            +
                # parameters array.
         
     | 
| 
      
 45 
     | 
    
         
            +
                default_values = keys.each_with_object({}) do |key_parameter, hsh|
         
     | 
| 
      
 46 
     | 
    
         
            +
                  hsh[key_parameter] = bnd.local_variable_get(key_parameter)
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                parameters.map do |mode, name|
         
     | 
| 
      
 50 
     | 
    
         
            +
                  [mode, name, default_values[name]]
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
              private
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
              # returns a minimal Array of arguments, which is suitable for a call to the method
         
     | 
| 
      
 57 
     | 
    
         
            +
              def minimal_arguments(method)
         
     | 
| 
      
 58 
     | 
    
         
            +
                # Build an arguments array with holds all required parameters. The actual
         
     | 
| 
      
 59 
     | 
    
         
            +
                # values for these arguments doesn't matter at all.
         
     | 
| 
      
 60 
     | 
    
         
            +
                args = method.parameters.select { |mode, _name| mode == :req }
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                # Add a hash with all required named arguments
         
     | 
| 
      
 63 
     | 
    
         
            +
                required_keyword_args = method.parameters.each_with_object({}) do |(mode, name), hsh|
         
     | 
| 
      
 64 
     | 
    
         
            +
                  hsh[name] = :anything if mode == :keyreq
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
                args << required_keyword_args if required_keyword_args
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                args
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,42 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require_relative "method_reflection"
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class ::Simple::Service::Action::Parameter
         
     | 
| 
      
 4 
     | 
    
         
            +
              def self.reflect_on_method(service:, name:)
         
     | 
| 
      
 5 
     | 
    
         
            +
                reflected_parameters = ::Simple::Service::Action::MethodReflection.parameters(service, name)
         
     | 
| 
      
 6 
     | 
    
         
            +
                @parameters = reflected_parameters.map { |ary| new(*ary) }
         
     | 
| 
      
 7 
     | 
    
         
            +
              end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              def keyword?
         
     | 
| 
      
 10 
     | 
    
         
            +
                [:keyreq, :key].include? @kind
         
     | 
| 
      
 11 
     | 
    
         
            +
              end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def positional?
         
     | 
| 
      
 14 
     | 
    
         
            +
                [:req, :opt].include? @kind
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              def required?
         
     | 
| 
      
 18 
     | 
    
         
            +
                [:req, :keyreq].include? @kind
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              def variadic?
         
     | 
| 
      
 22 
     | 
    
         
            +
                @kind == :rest
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              attr_reader :name
         
     | 
| 
      
 26 
     | 
    
         
            +
              attr_reader :kind
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              # The parameter's default value (if any)
         
     | 
| 
      
 29 
     | 
    
         
            +
              attr_reader :default_value
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              def initialize(kind, name, *default_value)
         
     | 
| 
      
 32 
     | 
    
         
            +
                # The parameter list matches the values returned from MethodReflection.parameters,
         
     | 
| 
      
 33 
     | 
    
         
            +
                # which has two or three entries: <tt>kind, name [ . default_value ]</tt>
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                expect! kind => [:req, :opt, :keyreq, :key, :rest]
         
     | 
| 
      
 36 
     | 
    
         
            +
                expect! default_value.length => [0, 1]
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                @kind = kind
         
     | 
| 
      
 39 
     | 
    
         
            +
                @name = name
         
     | 
| 
      
 40 
     | 
    
         
            +
                @default_value = default_value[0]
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,94 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Simple::Service
         
     | 
| 
      
 2 
     | 
    
         
            +
              # Returns the current context.
         
     | 
| 
      
 3 
     | 
    
         
            +
              def self.context
         
     | 
| 
      
 4 
     | 
    
         
            +
                Thread.current[:"Simple::Service.context"]
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              # yields a block with a given context, and restores the previous context
         
     | 
| 
      
 8 
     | 
    
         
            +
              # object afterwards.
         
     | 
| 
      
 9 
     | 
    
         
            +
              def self.with_context(ctx = nil, &block)
         
     | 
| 
      
 10 
     | 
    
         
            +
                old_ctx = Thread.current[:"Simple::Service.context"]
         
     | 
| 
      
 11 
     | 
    
         
            +
                new_ctx = old_ctx ? old_ctx.merge(ctx) : Context.new(ctx)
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                Thread.current[:"Simple::Service.context"] = new_ctx
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                block.call
         
     | 
| 
      
 16 
     | 
    
         
            +
              ensure
         
     | 
| 
      
 17 
     | 
    
         
            +
                Thread.current[:"Simple::Service.context"] = old_ctx
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            module Simple::Service
         
     | 
| 
      
 22 
     | 
    
         
            +
              # A context object
         
     | 
| 
      
 23 
     | 
    
         
            +
              #
         
     | 
| 
      
 24 
     | 
    
         
            +
              # Each service executes with a current context. The system manages a stack of
         
     | 
| 
      
 25 
     | 
    
         
            +
              # contexts; whenever a service execution is done the current context is reverted
         
     | 
| 
      
 26 
     | 
    
         
            +
              # to its previous value.
         
     | 
| 
      
 27 
     | 
    
         
            +
              #
         
     | 
| 
      
 28 
     | 
    
         
            +
              # A context object can store a large number of values; the only way to set or
         
     | 
| 
      
 29 
     | 
    
         
            +
              # access a value is via getters and setters. These are implemented via
         
     | 
| 
      
 30 
     | 
    
         
            +
              # +method_missing(..)+.
         
     | 
| 
      
 31 
     | 
    
         
            +
              #
         
     | 
| 
      
 32 
     | 
    
         
            +
              # Also, once a value is set in the context it is not possible to change or
         
     | 
| 
      
 33 
     | 
    
         
            +
              # unset it.
         
     | 
| 
      
 34 
     | 
    
         
            +
              class Context
         
     | 
| 
      
 35 
     | 
    
         
            +
                def initialize(hsh = {}) # :nodoc:
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @hsh = hsh
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # returns a new Context object, which merges the values in the +overlay+
         
     | 
| 
      
 40 
     | 
    
         
            +
                # argument (which must be a Hash or nil) with the values in this context.
         
     | 
| 
      
 41 
     | 
    
         
            +
                #
         
     | 
| 
      
 42 
     | 
    
         
            +
                # The overlay is allowed to change values in the current context.
         
     | 
| 
      
 43 
     | 
    
         
            +
                #
         
     | 
| 
      
 44 
     | 
    
         
            +
                # It does not change this context.
         
     | 
| 
      
 45 
     | 
    
         
            +
                def merge(overlay)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  expect! overlay => [Hash, nil]
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  overlay ||= {}
         
     | 
| 
      
 49 
     | 
    
         
            +
                  new_context_hsh = @hsh.merge(overlay)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  ::Simple::Service::Context.new(new_context_hsh)
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                private
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # :nodoc:
         
     | 
| 
      
 56 
     | 
    
         
            +
                IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # :nodoc:
         
     | 
| 
      
 57 
     | 
    
         
            +
                ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z") # :nodoc:
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                def method_missing(sym, *args, &block)
         
     | 
| 
      
 60 
     | 
    
         
            +
                  raise ArgumentError, "Block given" if block
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  if args.count == 0 && sym =~ IDENTIFIER_REGEXP
         
     | 
| 
      
 63 
     | 
    
         
            +
                    self[sym]
         
     | 
| 
      
 64 
     | 
    
         
            +
                  elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
         
     | 
| 
      
 65 
     | 
    
         
            +
                    self[$1.to_sym] = args.first
         
     | 
| 
      
 66 
     | 
    
         
            +
                  else
         
     | 
| 
      
 67 
     | 
    
         
            +
                    super
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                def respond_to_missing?(sym, include_private = false)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  # :nocov:
         
     | 
| 
      
 73 
     | 
    
         
            +
                  return true if IDENTIFIER_REGEXP.match?(sym)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  return true if ASSIGNMENT_REGEXP.match?(sym)
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  super
         
     | 
| 
      
 77 
     | 
    
         
            +
                  # :nocov:
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                def [](key)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  @hsh[key]
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                def []=(key, value)
         
     | 
| 
      
 85 
     | 
    
         
            +
                  existing_value = @hsh[key]
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                  unless existing_value.nil? || existing_value == value
         
     | 
| 
      
 88 
     | 
    
         
            +
                    raise ::Simple::Service::ContextReadOnlyError, key
         
     | 
| 
      
 89 
     | 
    
         
            +
                  end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  @hsh[key] = value
         
     | 
| 
      
 92 
     | 
    
         
            +
                end
         
     | 
| 
      
 93 
     | 
    
         
            +
              end
         
     | 
| 
      
 94 
     | 
    
         
            +
            end
         
     |