perfume 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cdbf43b151d7f376a9858e70e92c2a86dc815a73
4
+ data.tar.gz: 44e31ee400a397f536e36ee94eea30f7abe6fc06
5
+ SHA512:
6
+ metadata.gz: 1506aa517bcc89219aabaa16765d51f140fc5d841625f3f8f8a4a7813485f76057fc83e8608bfb8c316c039224ad474bac11efb4286703a200acb98e77a69a07
7
+ data.tar.gz: cf04e57c3e583617460867bae3d1b45df456daedac7e4206c576c34ee2ed829129bdf5204b24de088bf739a30e2a444af397db8eca361b7c9f011885c02e9e67
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ...
8
+
9
+ ## [0.1.0]
10
+
11
+ ### Added
12
+
13
+ - Project extracted from other library and moved into this standalone gem.
data/Dockerfile ADDED
@@ -0,0 +1,11 @@
1
+ FROM ruby:2.2
2
+
3
+ ENV GEM_NAME perfume
4
+ ENV WORKDIR /usr/local/src/$GEM_NAME
5
+
6
+ RUN mkdir -p $WORKDIR
7
+ WORKDIR $WORKDIR
8
+ ADD . $WORKDIR
9
+ RUN bundle install
10
+
11
+ CMD bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in perfume.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Rake Bump
2
+
3
+ [![Status](https://codeship.com/projects/xxx/status?branch=master)](https://codeship.com/projects/xxx)
4
+
5
+ Perfume is a set of fragrances to make your (micro)services code smell nice. It's bunch of common practices and
6
+ shorthands to deal with common problems. At the moment it includes the following stuff:
7
+
8
+ * So called SuperObject - a lightweight hash-initialized struct-like object.
9
+ * So called Service - a callable object that's designed to implement your business processes.
10
+ * So called Promise - extended Service with ignorable errors.
11
+ * Unified and pre-configured logging (adapted Log4r).
12
+ * Shell services.
13
+ * Console interaction service.
14
+ * Exit (abort) service.
15
+ * Various core extensions.
16
+ * Testing utilities.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'perfume', '0.1.0'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install perfume --version 0.1.0
33
+
34
+ ## Usage
35
+
36
+ TODO: ...
37
+
38
+ ## Development
39
+
40
+ You have two options to work with this project. The [docker flow](#setup-with-docker) is suggested since solves problems of compatibility of tools.
41
+
42
+ ### Manual Setup
43
+
44
+ First off, make sure you have **Ruby 2.2+** and latest version of **Bundler** on your machine. After checking out the repo, you can install dependencies and prepare the project with:
45
+
46
+ $ bin/setup
47
+
48
+ Now you can run tests:
49
+
50
+ $ bundle exec rake spec
51
+
52
+ You can also connect to interactive prompt that will allow you to experiment. To do this, run:
53
+
54
+ $ bundle exec bin/console
55
+
56
+ To install this gem onto your local machine, run:
57
+
58
+ $ bundle exec rake install
59
+
60
+ ### Setup with Docker
61
+
62
+ If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully [dockerized](http://docker.io/). Install docker toolchain and then go for:
63
+
64
+ $ docker-compose build
65
+
66
+ All done, you can do testing and fiddling around:
67
+
68
+ $ docker-compose run perfume bash
69
+ root@xyyyyxx:/usr/local/src/perfume# bundle exec rake spec
70
+ root@xyyyyxx:/usr/local/src/perfume# bundle exec bin/console
71
+
72
+ ### Releasing new version
73
+
74
+ This project is powered by rake-bump. To release gem version, follow [this continuous releasing guide](https://github.com/jobandtalent/rake-bump#continuous-releasing)
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and pull requests are welcome [here](https://github.com/kkvlk/perfume/issues).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "perfume/version"
3
+ # require "rake/bump/tasks"
4
+ require "rake/testtask"
5
+
6
+ # Rake::Bump::Tasks.new do |t|
7
+ # t.gem_name = 'perfume'
8
+ # t.gem_current_version = Perfume::VERSION
9
+ # end
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList['test/**/*_test.rb']
15
+ end
16
+
17
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "perfume"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ perfume:
2
+ build: .
3
+ volumes:
4
+ - .:/usr/local/src/perfume:rw
@@ -0,0 +1,9 @@
1
+ module Perfume
2
+ require 'perfume/logging'
3
+ require 'perfume/service'
4
+ require 'perfume/promise'
5
+ require 'perfume/shell'
6
+ require 'perfume/exit'
7
+ require 'perfume/console'
8
+ require 'perfume/core_ext/dir'
9
+ end
@@ -0,0 +1,62 @@
1
+ require 'perfume/super_object'
2
+
3
+ module Perfume
4
+ # Public: Sometimes there's a need to ask user a question or to get stuff from him.
5
+ # This simple class handles this. You can initialize this virtual "Console" pointed to
6
+ # given input and output stream and perform common operations like getting user input,
7
+ # asking questions (yes/no questions too), etc.
8
+ class Console < SuperObject
9
+ args :input, :output
10
+
11
+ def defaults
12
+ { input: STDIN, output: STDOUT }
13
+ end
14
+
15
+ # Public: Reads user input from given input stream. The answer can be either obtained
16
+ # from result or yielded value.
17
+ def read_user_input(&block)
18
+ value = @input.gets.to_s.strip
19
+ block_given? ? block.call(value) : value
20
+ end
21
+
22
+ # Public: Asks user a question. As with #read_user_input, the answer can be obtained
23
+ # from both, return value and yielded argument.
24
+ #
25
+ # Example
26
+ #
27
+ # console = Console.new
28
+ # console.ask("How much do you know?") do |answer|
29
+ # puts "So you know #{answer}"
30
+ # end
31
+ #
32
+ # $ How much do you know?: nothing
33
+ # $ So you know nothing...
34
+ #
35
+ def ask(question, &block)
36
+ @output.write(question + ': ')
37
+ read_user_input(&block)
38
+ end
39
+
40
+ # Public: Same like #ask, but converts the answer to boolean value. It also formats
41
+ # the question a little bit to indicate that we're asking about the boolean. Any other
42
+ # answer than case insensitive "y" or "yes" will be converted to false.
43
+ #
44
+ # Example
45
+ #
46
+ # console = Console.new
47
+ # console.ask("Are you Jon Snow?") do |answer|
48
+ # puts "Answer: #{answer.inspect}"
49
+ # end
50
+ #
51
+ # $ Are you Jon Snow? [y/N]: Yes
52
+ # $ Answer: true
53
+ #
54
+ def ask_yes_or_no(question, &block)
55
+ ask(question + ' [y/N]') do |answer|
56
+ %[y yes].include?(answer.downcase).tap do |bool_answer|
57
+ yield bool_answer if block_given?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ module Perfume
2
+ module CoreExt
3
+ module Dir
4
+ # Public: A helper that's a fusion of Dir.mktmpdir and Dir.chdir. Lazieness FTW!
5
+ def chtmpdir(&block)
6
+ ::Dir.mktmpdir do |dir|
7
+ ::Dir.chdir(dir) do
8
+ yield dir
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Dir.extend(Perfume::CoreExt::Dir)
@@ -0,0 +1,23 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ # Public: There's often need to exit from the app in a testable and graceful manner.
5
+ # Simple call to Kernel#exit isn't such. This simple wrapper around the Kernel method
6
+ # allows to say goodbye message via service logging as well as status code.
7
+ class Exit < Service
8
+ args :message, :code, :log
9
+
10
+ def defaults
11
+ { message: "Exiting...", code: 1 }
12
+ end
13
+
14
+ def log
15
+ @log ||= self.class.log
16
+ end
17
+
18
+ def call
19
+ log.error(@message)
20
+ Kernel.exit(@code)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Perfume
2
+ module Logging
3
+ # Internal: For command line apps we rather don't want to display all the logging information
4
+ # to the user. Just to avoid noise, INFO messages are displayed as is, and any other log levels
5
+ # are prefixed with the level name. Example:
6
+ #
7
+ # This is simple info message.
8
+ # And another one.
9
+ # WARN: Uups, something's not right here...
10
+ # And another info message.
11
+ # DEBUG: We need to go deeper!
12
+ # ERROR: Limbo!
13
+ #
14
+ class CommandLineOutputFormatter < Log4r::Formatter
15
+ def initialize
16
+ @info_formatter = Log4r::PatternFormatter.new(:pattern => "%m")
17
+ @else_formatter = Log4r::PatternFormatter.new(:pattern => "%l: %m")
18
+ end
19
+
20
+ def format(logevent)
21
+ (Log4r::INFO == logevent.level ? @info_formatter : @else_formatter).format(logevent)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module Perfume
2
+ module Logging
3
+ # Internal: Adapter for Log4r logging system. It's a poor version of what Slf4j does to Log4j in
4
+ # java apps. Just tiny layer to improve on logging payloads.
5
+ class Log4rAdapter
6
+ class Event
7
+ def initialize(message, payload)
8
+ @message, @payload = message, payload || {}
9
+ @payload_formatted = @payload.map { |k,v| "#{k}=#{v.inspect}" }.join(" ")
10
+ @s = [ @message, @payload_formatted.empty? ? nil : @payload_formatted].compact.join("; ")
11
+ end
12
+
13
+ def to_s
14
+ @s
15
+ end
16
+ end
17
+
18
+ def initialize(log)
19
+ @log = log
20
+ end
21
+
22
+ def method_missing(method, *args, &block)
23
+ @log.send(method, *args, &block)
24
+ end
25
+
26
+ %w[debug info warn error fatal].each do |level|
27
+ define_method(level) do |obj_or_msg, payload=nil|
28
+ @log.send(level, obj_or_msg.is_a?(String) ? Event.new(obj_or_msg, payload).to_s : obj_or_msg)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ require 'log4r'
2
+ require 'perfume/logging/log4r_adapter'
3
+
4
+ module Perfume
5
+ module Logging
6
+ # Public: Similarly to Perfume::Logging no point to initialize global logger for your
7
+ # modules/packages. Include this mixin to define LOGGER constant and log class method
8
+ # shortcut.
9
+ #
10
+ # Often used practice is to define such top level logger as a parent with default
11
+ # outputters, formatters, log level, etc.
12
+ module PackageLogger
13
+ def self.included(klass)
14
+ klass.const_set(:LOGGER, Logging::Log4rAdapter.new(Log4r::Logger.new(klass.name)))
15
+ klass.extend(ClassMethods)
16
+ end
17
+
18
+ module ClassMethods
19
+ def log
20
+ const_get(:LOGGER)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require 'log4r'
2
+
3
+ module Perfume
4
+ # Public: Log4r is awesome, but why initialize it for every class manually if you
5
+ # can use simple extension that will do it for you. This one injects class and
6
+ # instance `log` methods to access class logger.
7
+ module Logging
8
+ require 'perfume/logging/log4r_adapter'
9
+ require 'perfume/logging/command_line_output_formatter'
10
+ require 'perfume/logging/package_logger'
11
+
12
+ def self.included(klass)
13
+ klass.extend(ClassMethods)
14
+ end
15
+
16
+ def log
17
+ self.class.log
18
+ end
19
+
20
+ module ClassMethods
21
+ def log
22
+ @log ||= Log4rAdapter.new(Log4r::Logger.new(self.name))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,121 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ # Public: Very often we're in a situation that we have to catch and eventuall log some
5
+ # errors, or even more, solely catch errors for purpose of logging. Other times we must
6
+ # perform multiple actions in different places according to the results produces by our
7
+ # method or service. We'd normally have to pass these results around deppening our stack
8
+ # trace. Here comes this a bit twisted implementation of a promise. Maybe twisted is
9
+ # wrong description, it's rather simplified. It's not parallel, it's simple to the bones.
10
+ # It just allows you to define three kinds of callbacks: before call, on success and
11
+ # on failure of course. Since it inherits from our own Service, it essentially is one
12
+ # with own logging and access to class level call method.
13
+ #
14
+ # Here's few interesting properties:
15
+ #
16
+ # 1. Define your logic in #call! method. Yes call with bang.
17
+ #
18
+ # class FindWally < Perfume::Promise
19
+ # NotFoundWarning = Class.new(Warning)
20
+ #
21
+ # def call!
22
+ # Wally.latest_location.tap do |location|
23
+ # raise NotFoundWarning, "No idea where is Wally!" if location.nil?
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # 2. Failures are dependent on the kind of error thrown. It means that the flow
29
+ # is directed by raised errors. Slow you might say, but benchmarking this shows that
30
+ # the overhead is negligible in real life apps. This also allows a nifty trick. Look
31
+ # at the example of `FindWally` above. Why there's `NotFoundWarning` inhertiting from
32
+ # magical `Warning` class? The `Warning` is bundled into each new promise defined.
33
+ # This errors are not thrown like regular exceptions. Instead they're handled and
34
+ # logged as warnings. This error will be passed to our `failure` callbacks as well.
35
+ # Check example:
36
+ #
37
+ # FindWally.call do |_location|
38
+ # raise StandardError, "Something went wrong!"
39
+ # end
40
+ #
41
+ # This one raises `StandardError`, while this one:
42
+ #
43
+ # find_wally = FindWally.new
44
+ # find_wally.failure { |err| puts err.message }
45
+ # find_wally.call { |_location| raise FindWally::Warning, "Uuups!" }
46
+ #
47
+ # Will log WARNING with "Uuups!" message.
48
+ #
49
+ # 3. Block passed to call is added to on success callbacks:
50
+ #
51
+ # FindWally.call do |location|
52
+ # puts location
53
+ # end
54
+ #
55
+ # $ "Madrid"
56
+ #
57
+ class Promise < Service
58
+ Warning = Class.new(Exception)
59
+
60
+ def self.inherited(child)
61
+ child.const_set(:Warning, Warning)
62
+ end
63
+
64
+ def initialize(*)
65
+ super
66
+
67
+ @__before = []
68
+ @__on_success = []
69
+ @__on_failure = []
70
+ end
71
+
72
+ def before(&block)
73
+ @__before << block
74
+ self
75
+ end
76
+
77
+ def success(&block)
78
+ @__on_success << block
79
+ self
80
+ end
81
+
82
+ def failure(&block)
83
+ @__on_failure << block
84
+ self
85
+ end
86
+
87
+ def success?
88
+ @__ok
89
+ end
90
+
91
+ def failure?
92
+ !success?
93
+ end
94
+
95
+ # Public: Your logic goes here. Flow is broken by raising an exception of local Error
96
+ # class or child classes. Any return value will be passed to on-success callbacks.
97
+ def call!
98
+ raise NotImplementedError
99
+ end
100
+
101
+ # Pubic: Safely executes your logic defined in call!, taking care that all callbacks
102
+ # are properly called.
103
+ def call(&block)
104
+ @__ok = false
105
+ @__before.each(&:call)
106
+ success_callbacks = @__on_success + [block]
107
+ call!.tap { |result| success_callbacks.compact.each { |callback| callback.call(result) } }
108
+ @__ok = true
109
+ self
110
+ rescue Warning => err
111
+ log.warn(err)
112
+ @__on_failure.each { |callback| callback.call(err) }
113
+ return nil
114
+ end
115
+ end
116
+
117
+ # Public: Shorthand to define a Promise with accessors.
118
+ def self.Promise(*names, &block)
119
+ Class.new(Promise, &block).tap { |klass| klass.args_accessor(*names) }
120
+ end
121
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'perfume/super_object'
3
+
4
+ module Perfume
5
+ # Public: The pattern is simple, our Service is a struct with init arguments that
6
+ # additionally is callable. It also comes with class level `call` method that
7
+ # executes stuff on new instance of the object.
8
+ class Service < SuperObject
9
+ include Logging
10
+
11
+ # Public: Shorthand to instantionante with arguments and perform call in one shot.
12
+ # You should actually always intend to use this method as it's easier to mock
13
+ # in testing.
14
+ def self.call(args = {}, &block)
15
+ new(args).call(&block)
16
+ end
17
+
18
+ # Public: Implement this one on your own. Your logic goes here.
19
+ def call(&block)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ protected
24
+
25
+ def system_call(*args, &block)
26
+ Shell::SystemCall.call(*args, &block)
27
+ end
28
+
29
+ def exec(*args, &block)
30
+ Shell::Exec.call(*args, &block)
31
+ end
32
+ end
33
+
34
+ # Public: Shorthand to define a Service with accessors.
35
+ def self.Service(*names, &block)
36
+ Class.new(Service, &block).tap { |klass| klass.args_accessor(*names) }
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ require 'perfume/service'
2
+
3
+ module Perfume
4
+ module Shell
5
+ # Internal: Executing shell commands isn't good practice. Especially it's bad when
6
+ # calling ruby methods like `exec` or `system`. This is dangerous and completely
7
+ # untestable. So here it comes base wrapper around shell calls.
8
+ #
9
+ # Check Perfume::Shell::Exec and Perfume::Shell:SystemCall for two most common
10
+ # use cases.
11
+ class Base < Perfume::Service(:cmd, :root, :log)
12
+ def init
13
+ @root = Pathname.new(@root)
14
+ end
15
+
16
+ def log
17
+ @log or self.class.log
18
+ end
19
+
20
+ def defaults
21
+ { root: Dir.pwd }
22
+ end
23
+
24
+ def before
25
+ end
26
+
27
+ def to_s
28
+ cmd.to_s
29
+ end
30
+
31
+ protected
32
+
33
+ def fail!(msg = "Shell command execution failed!")
34
+ raise ExecutionError, msg
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Go for Exec when you need to obtain standard output of the command. It uses
4
+ # the %x[] call under the hood. Note that it doesn't raise errors when return status
5
+ # is different than 0. Same as with Perfume::Shell::SystemCall, you should go for
6
+ # inheriting from this class:
7
+ #
8
+ # class GitDirtyTreeValidation < Perfume::Shell::Exec
9
+ # Error = Class.new(StandardError)
10
+ #
11
+ # def cmd
12
+ # 'git status -s'
13
+ # end
14
+ #
15
+ # def fail_on_error?
16
+ # true
17
+ # end
18
+ #
19
+ # def handle
20
+ # raise Error, "Uncommited changes detected!" unless output.strip.empty?
21
+ # end
22
+ # end
23
+ #
24
+ class Exec < Base
25
+ args :fail_on_error
26
+ attr_reader :output, :rc
27
+
28
+ def init
29
+ @__done = []
30
+ @fail_on_error = false if @fail_on_error.nil?
31
+ end
32
+
33
+ def fail_on_error?
34
+ @fail_on_error
35
+ end
36
+
37
+ def call(&block)
38
+ Dir.chdir(@root.to_s) do
39
+ before
40
+ log.debug("Executing shell command and waiting for result", cmd: cmd, root: @root.to_s)
41
+ @output, @rc = [ %x[#{cmd} 2>&1], $?.to_i ]
42
+ fail! if fail_on_error? and @rc != 0
43
+ result = respond_to?(:handle) ? handle : [ @output, @rc ]
44
+ yield result if block_given?
45
+ return result
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def fail!(*)
52
+ $stderr.write(@output)
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Simple system call. Use it when you need to just execute something, eventually
4
+ # silently and you don't care about the result. Uses `system` under the hood. Feel free
5
+ # to inherit from this class, example:
6
+ #
7
+ # class GitCommit < Perfume::Shell::SystemCall
8
+ # args :message
9
+ #
10
+ # def cmd
11
+ # 'git commit -m #{@message.inspect}'
12
+ # end
13
+ # end
14
+ #
15
+ class SystemCall < Base
16
+ args :quiet
17
+
18
+ def defaults
19
+ super.merge(quiet: false)
20
+ end
21
+
22
+ def call
23
+ Dir.chdir(@root.to_s) do
24
+ before
25
+ log.debug("Executing shell command", cmd: cmd, root: @root.to_s)
26
+ system([ cmd, !!@quiet ? ' &>/dev/null' : nil ].compact.join(' '))
27
+ fail! unless $?.to_i == 0
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module Perfume
2
+ module Shell
3
+ # Public: Raised when shell command returned non-zero exit status.
4
+ ExecutionError = Class.new(StandardError)
5
+
6
+ require 'perfume/shell/base'
7
+ require 'perfume/shell/system_call'
8
+ require 'perfume/shell/exec'
9
+ end
10
+ end
@@ -0,0 +1,93 @@
1
+ module Perfume
2
+ # Public: Premise is tha using method arguments sucks because you need to remember the order,
3
+ # sucks even more when you might have lot of them (like structure/service initializers).
4
+ #
5
+ # It's often seen in ruby to declare classes that inherit from Struct. There we have problem
6
+ # of arguments order. Maybe I wanna specify defaults? Maybe I wanna pass only one argument
7
+ # from the middle of the list? For such cases people use OpenStruct or third-party stuff
8
+ # like Hashie::Dash. Not a good idea either because those classes are bloated with useless
9
+ # methods.
10
+ #
11
+ # Here's how this super object works:
12
+ #
13
+ # class Women < SuperObject(:name, :surname)
14
+ # args :married
15
+ #
16
+ # def title
17
+ # @married ? 'Mrs.' : 'Miss'
18
+ # end
19
+ #
20
+ # def to_s
21
+ # [ title, name, surname ].join(' ')
22
+ # end
23
+ # end
24
+ #
25
+ # ada = Women.new(name: "Ada", surname: "Lovelace", married: true)
26
+ # ada.name # => "Ada"
27
+ # ada.surname # => "Lovelace"
28
+ # ada.to_s # => "Mrs. Ada Lovelace"
29
+ #
30
+ # mary = Women.new(name: "mary", surname: "Cassat", married: false)
31
+ # mary.to_s # => "Miss Mary Cassat"
32
+ # mary.married # => NoMethodError
33
+ #
34
+ # Women.new(unknown: 'Something') # => ArgumentError
35
+ #
36
+ # By default `args` method defines only instance variables. Then we have `args_accessor`,
37
+ # which is called for all class parameters, `args_reader` and `args_writer` if you need
38
+ # to expose some stuff.
39
+ class SuperObject
40
+ # Public: Returns list of defined arguments. Note that superclass arguments are inherited
41
+ # by child class.
42
+ def self.init_args
43
+ @init_args ||= superclass.respond_to?(:init_args) ? superclass.init_args.dup : []
44
+ end
45
+
46
+ # Public: Defines unaccessible arguments.
47
+ def self.args(*names)
48
+ init_args.concat(names)
49
+ end
50
+
51
+ # Public: Defines given init arguments alongside with accessor methods.
52
+ def self.args_accessor(*names)
53
+ names = names.map(&:to_sym)
54
+ args(*names)
55
+ attr_accessor(*names)
56
+ end
57
+
58
+ # Public: Defines given init arguments alongside with reader method.
59
+ def self.args_reader(*names)
60
+ args(*names)
61
+ attr_reader(*names)
62
+ end
63
+
64
+ # Public: Defines given init arguments alongside with writer method.
65
+ def self.args_writer(*names)
66
+ args(*names)
67
+ attr_writer(*names)
68
+ end
69
+
70
+ def initialize(args = {})
71
+ args.symbolize_keys!
72
+ unknown_args = args.keys - self.class.init_args
73
+ raise ArgumentError, "Unknown arguments: #{unknown_args.map(&:inspect).join(', ')}" unless unknown_args.empty?
74
+ args = defaults.symbolize_keys.merge(args)
75
+ self.class.init_args.each { |name| instance_variable_set("@#{name}", args[name]) }
76
+ init
77
+ end
78
+
79
+ # Public: Extra initialization.
80
+ def init
81
+ end
82
+
83
+ # Public: Override it with your own default arguments.
84
+ def defaults
85
+ {}
86
+ end
87
+ end
88
+
89
+ # Public: Shorthand to define a super object with accessors.
90
+ def self.SuperObject(*names, &block)
91
+ Class.new(SuperObject, &block).tap { |klass| klass.args_accessor(*names) }
92
+ end
93
+ end
@@ -0,0 +1,38 @@
1
+ require 'pathname'
2
+ require 'forwardable'
3
+
4
+ module Perfume
5
+ module Testing
6
+ # Public: Sometimes you have to test file contents or some biggest chunks of data. Put them in fixtures
7
+ # folder and load using this helper class. Example:
8
+ #
9
+ # MY_FIXTURE_FILES = FixtureFiles.load('path/to/fixtures/*.txt')
10
+ # MY_FIXTURE_FILES.each { |content| ... }
11
+ # puts MY_FIXTURE_FILES['filename.txt']
12
+ #
13
+ class FixtureFiles
14
+ extend Forwardable
15
+ def_delegators :@fixtures, :[], :size, :each
16
+
17
+ include Enumerable
18
+
19
+ class << self
20
+ alias :load :new
21
+ end
22
+
23
+ def initialize(dir)
24
+ @fixtures = load_fixtures(dir)
25
+ end
26
+
27
+ private
28
+
29
+ def load_fixtures(dir)
30
+ {}.tap do |fixtures|
31
+ Pathname.glob(dir.to_s) do |entry|
32
+ fixtures[entry.basename.to_s] = entry.read if entry.file?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'pathname'
2
+
3
+ module Perfume
4
+ module Testing
5
+ # Public: You load support files in pretty much every test/spec helper. It's stupid to repeat yourself
6
+ # like this. Use following helper to load support stuff from given directory.
7
+ module Support
8
+ def self.require_all(dir)
9
+ Dir.glob(dir.join('*.rb').to_s) do |f|
10
+ require f
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module Perfume
2
+ module Testing
3
+ require 'perfume/testing/support'
4
+ require 'perfume/testing/fixture_files'
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Perfume
2
+ VERSION = '0.1.0'
3
+ end
data/lib/perfume.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Perfume
2
+ require 'perfume/version'
3
+ end
data/perfume.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'perfume/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "perfume"
8
+ spec.version = Perfume::VERSION
9
+ spec.authors = ["jobandtalent", "Kris Kovalik"]
10
+ spec.email = ["kris.kovalik@jobandtalent.com", "hi@kkvlk.me"]
11
+
12
+ spec.summary = %q{Tooling for writing (micro)services in Ruby.}
13
+ spec.description = %q{Bunch of tools and utilities for common day-to-day tasks while working with microservices.!}
14
+ spec.homepage = "https://github.com/jobandtalent/perfume"
15
+ spec.license = 'Apache-2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.8"
25
+ spec.add_development_dependency "minitest-reporters", "~> 1.1"
26
+ spec.add_development_dependency "mocha", "~> 1.1"
27
+ # spec.add_development_dependency "rake-bump", "~> 0.4"
28
+
29
+ spec.add_dependency "activesupport", "~> 4.2", ">= 3.0"
30
+ spec.add_dependency "log4r", "~> 1.1", ">= 1.1"
31
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perfume
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jobandtalent
8
+ - Kris Kovalik
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-01-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: minitest
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '5.8'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '5.8'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest-reporters
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.1'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.1'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.1'
84
+ - !ruby/object:Gem::Dependency
85
+ name: activesupport
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '4.2'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '3.0'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '4.2'
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: log4r
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.1'
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '1.1'
114
+ type: :runtime
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '1.1'
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '1.1'
124
+ description: Bunch of tools and utilities for common day-to-day tasks while working
125
+ with microservices.!
126
+ email:
127
+ - kris.kovalik@jobandtalent.com
128
+ - hi@kkvlk.me
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".editorconfig"
134
+ - ".gitignore"
135
+ - ".rspec"
136
+ - CHANGELOG.md
137
+ - Dockerfile
138
+ - Gemfile
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/setup
143
+ - docker-compose.yml
144
+ - lib/perfume.rb
145
+ - lib/perfume/all.rb
146
+ - lib/perfume/console.rb
147
+ - lib/perfume/core_ext/dir.rb
148
+ - lib/perfume/exit.rb
149
+ - lib/perfume/logging.rb
150
+ - lib/perfume/logging/command_line_output_formatter.rb
151
+ - lib/perfume/logging/log4r_adapter.rb
152
+ - lib/perfume/logging/package_logger.rb
153
+ - lib/perfume/promise.rb
154
+ - lib/perfume/service.rb
155
+ - lib/perfume/shell.rb
156
+ - lib/perfume/shell/base.rb
157
+ - lib/perfume/shell/exec.rb
158
+ - lib/perfume/shell/system_call.rb
159
+ - lib/perfume/super_object.rb
160
+ - lib/perfume/testing.rb
161
+ - lib/perfume/testing/fixture_files.rb
162
+ - lib/perfume/testing/support.rb
163
+ - lib/perfume/version.rb
164
+ - perfume.gemspec
165
+ homepage: https://github.com/jobandtalent/perfume
166
+ licenses:
167
+ - Apache-2.0
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.5.1
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Tooling for writing (micro)services in Ruby.
189
+ test_files: []