perfume 0.1.0
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/.editorconfig +12 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +13 -0
- data/Dockerfile +11 -0
- data/Gemfile +4 -0
- data/README.md +78 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +4 -0
- data/lib/perfume/all.rb +9 -0
- data/lib/perfume/console.rb +62 -0
- data/lib/perfume/core_ext/dir.rb +16 -0
- data/lib/perfume/exit.rb +23 -0
- data/lib/perfume/logging/command_line_output_formatter.rb +25 -0
- data/lib/perfume/logging/log4r_adapter.rb +33 -0
- data/lib/perfume/logging/package_logger.rb +25 -0
- data/lib/perfume/logging.rb +26 -0
- data/lib/perfume/promise.rb +121 -0
- data/lib/perfume/service.rb +38 -0
- data/lib/perfume/shell/base.rb +38 -0
- data/lib/perfume/shell/exec.rb +57 -0
- data/lib/perfume/shell/system_call.rb +32 -0
- data/lib/perfume/shell.rb +10 -0
- data/lib/perfume/super_object.rb +93 -0
- data/lib/perfume/testing/fixture_files.rb +38 -0
- data/lib/perfume/testing/support.rb +15 -0
- data/lib/perfume/testing.rb +6 -0
- data/lib/perfume/version.rb +3 -0
- data/lib/perfume.rb +3 -0
- data/perfume.gemspec +31 -0
- metadata +189 -0
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
data/.gitignore
ADDED
data/.rspec
ADDED
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
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Rake Bump
|
2
|
+
|
3
|
+
[](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
data/docker-compose.yml
ADDED
data/lib/perfume/all.rb
ADDED
@@ -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)
|
data/lib/perfume/exit.rb
ADDED
@@ -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,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
|
data/lib/perfume.rb
ADDED
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: []
|