Skip to content
This repository has been archived by the owner on Apr 6, 2021. It is now read-only.

Commit

Permalink
Use singletons for Env and TaskEnv, multitons for Project.
Browse files Browse the repository at this point in the history
As a result, the plugin method #wants_projects_matching has been
refactored to return Project instances now so that Plugins can define
the parents of a node (among other things).#wants_projects_matching has
been refactored to return Project instances now so that Plugins can
define the parents of a node (among other
things).#wants_projects_matching has been refactored to return Project
instances now so that Plugins can define the parents of a node (among
other things).
  • Loading branch information
nathankleyn committed May 30, 2015
1 parent fd7a188 commit 5be04c1
Show file tree
Hide file tree
Showing 25 changed files with 128 additions and 132 deletions.
7 changes: 7 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ require: rubocop-rspec

LineLength:
Max: 120

# This is a stupid lint. Yes, module_function is more explanatory than extend self for most use cases. However, the
# former does not work if you want the methods to be able to call private methods; the latter does. See
# https://practicingruby.com/articles/ruby-and-the-singleton-pattern-dont-get-along for a detailed explanation of the
# problem.
ModuleFunction:
Enabled: False
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.4)
rubocop-rspec (1.3.0)
ruby-prof (0.15.8)
ruby-progressbar (1.7.5)
simplecov (0.10.0)
docile (~> 1.1.0)
Expand All @@ -106,4 +107,5 @@ DEPENDENCIES
rspec (~> 3.2)
rubocop (~> 0.31)
rubocop-rspec (~> 1.3)
ruby-prof (~> 0.15)
shanty!
2 changes: 2 additions & 0 deletions examples/test-static-project/Shantyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
option 'foo', 'bar'
tag 'lux'
10 changes: 4 additions & 6 deletions lib/shanty.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'i18n'
require 'pathname'
require 'pry'

require 'shanty/cli'
require 'shanty/env'
Expand All @@ -12,13 +11,16 @@
module Shanty
# Main shanty class
class Shanty
include Env

# This is the root directory where the Shanty gem is located. Do not confuse this with the root of the repository
# in which Shanty is operating, which is available via the TaskEnv class.
GEM_ROOT = File.expand_path(File.join(__dir__, '..'))

def start!
setup_i18n
Cli.new(env, TaskSet.task_sets).run
require!
Cli.new(TaskSet.task_sets).run
end

private
Expand All @@ -27,9 +29,5 @@ def setup_i18n
I18n.enforce_available_locales = true
I18n.load_path = Dir[File.join(GEM_ROOT, 'translations', '*.yml')]
end

def env
Env.new.tap(&:require!)
end
end
end
12 changes: 3 additions & 9 deletions lib/shanty/cli.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'commander'
require 'i18n'
require 'shanty/info'
require 'shanty/task_env'
require 'shanty/task_set'

module Shanty
Expand All @@ -10,17 +9,12 @@ module Shanty
class Cli
include Commander::Methods

attr_reader :env, :task_sets
attr_reader :task_sets

def initialize(env, task_sets)
@env = env
def initialize(task_sets)
@task_sets = task_sets
end

def task_env
@task_env ||= TaskEnv.new(@env)
end

def tasks
@tasks ||= task_sets.reduce({}) do |acc, task_set|
# FIXME: Warn or fail when there are duplicate task names?
Expand Down Expand Up @@ -70,7 +64,7 @@ def add_action_to_command(name, task, command)
end

def execute_task(name, task, options)
klass = task[:klass].new(task_env)
klass = task[:klass].new
arity = klass.method(name).arity
args = (arity >= 1 ? [options] : [])
klass.send(name, *args)
Expand Down
13 changes: 12 additions & 1 deletion lib/shanty/env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@

module Shanty
#
class Env
module Env
# Idiom to allow singletons that can be mixed in: http://ozmm.org/posts/singin_singletons.html
extend self

CONFIG_FILE = '.shanty.yml'

def clear!
@logger = nil
@environment = nil
@build_number = nil
@root = nil
@config = nil
end

def require!
Dir.chdir(root) do
(config['require'] || {}).each do |path|
Expand Down
53 changes: 30 additions & 23 deletions lib/shanty/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
require 'shanty/env'
require 'shanty/project'

module Shanty
# Some basic functionality for every plugin.
module Plugin
include Env

def self.extended(plugin)
plugins << plugin
end
Expand All @@ -9,23 +14,19 @@ def self.plugins
@plugins ||= []
end

def self.discover_all_projects(env) # => Hash[Path, Array[Plugin]] where Path = String, Plugin = Class
projects = Hash.new { |h, k| h[k] = [] }
plugins.each_with_object(projects) do |plugin, acc|
paths = plugin.discover_projects(env) + plugin.wanted_projects(env)
paths.each { |path| acc[path] << plugin }
end
#

def self.discover_all_projects # => [Project]
plugins.flat_map(&:wanted_projects).uniq
end

def self.with_graph(env, graph)
def self.with_graph(graph)
plugins.each do |plugin|
plugin.with_graph_callbacks.each { |callback| callback.call(env, graph) }
plugin.with_graph_callbacks.each { |callback| callback.call(graph) }
end
end

def discover_projects(_env)
[]
end
#

def callbacks
@callbacks ||= []
Expand All @@ -47,17 +48,21 @@ def with_graph_callbacks
@with_graph_callbacks ||= []
end

#

def add_to_project(project)
project.singleton_class.include(self)
callbacks.each { |args| project.subscribe(*args) }
tags.each { |tag| project.tag(tag) }
end

#

def subscribe(*args)
callbacks << args
end

def add_tags(*args)
def adds_tags(*args)
tags.concat(args)
end

Expand All @@ -70,27 +75,29 @@ def with_graph(&block)
with_graph_callbacks << block
end

def wanted_projects(env)
(wanted_projects_from_globs(env) + wanted_projects_from_callbacks(env)).uniq
#

def wanted_projects
(wanted_projects_from_globs + wanted_projects_from_callbacks).uniq.tap do |projects|
projects.each do |project|
project.plugin(self)
end
end
end

private

def wanted_projects_from_globs(env)
def wanted_projects_from_globs
wanted_project_globs.flat_map do |globs|
# Will make the glob absolute to the root if (and only if) it is relative.
Dir[File.expand_path(globs, env.root)].map do |path|
File.absolute_path(File.dirname(path))
Dir[File.expand_path(globs, root)].map do |path|
Project.new(File.absolute_path(File.dirname(path)))
end
end
end

def wanted_projects_from_callbacks(env)
wanted_project_callbacks.flat_map do |callback|
callback.call(env).map do |path|
File.absolute_path(File.dirname(path))
end
end
def wanted_projects_from_callbacks
wanted_project_callbacks.flat_map(&:call)
end
end
end
2 changes: 1 addition & 1 deletion lib/shanty/plugins/bundler_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Shanty
module BundlerPlugin
extend Plugin

add_tags :bundler
adds_tags :bundler
wants_projects_matching '**/Gemfile'
subscribe :build, :bundle_install

Expand Down
5 changes: 3 additions & 2 deletions lib/shanty/plugins/rspec_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ module Shanty
module RspecPlugin
extend Plugin

add_tags :rspec
adds_tags :rspec
# By default, we'll detect RSpec in a project if has a dependency on it in a Gemfile or *.gemspec file. If you don't
# use these files, you'll need to import the plugin manually using a Shantyfile as we can't tell if RSpec is being
# used from the presence of a spec directory alone (this can be many other testing frameworks!)
wants_projects_matching do
Dir['**/{*.gemspec,Gemfile}'].each_with_object([]) do |dependency_file, acc|
acc << File.dirname(dependency_file) if File.read(dependency_file) =~ /['"]rspec['"]/
next unless File.read(dependency_file) =~ /['"]rspec['"]/
acc << Project.new(File.absolute_path(File.dirname(dependency_file)))
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/shanty/plugins/rubocop_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Shanty
module RubocopPlugin
extend Plugin

add_tags :rubocop
adds_tags :rubocop
wants_projects_matching '**/.rubocop.yml'
subscribe :test, :rubocop

Expand Down
2 changes: 1 addition & 1 deletion lib/shanty/plugins/rubygem_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Shanty
module RubygemPlugin
extend Plugin

add_tags :rubygem
adds_tags :rubygem
wants_projects_matching '**/*.gemspec'
subscribe :build, :build_gem

Expand Down
2 changes: 1 addition & 1 deletion lib/shanty/plugins/shantyfile_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Shanty
module ShantyfilePlugin
extend Plugin

add_tags :shantyfile
adds_tags :shantyfile
wants_projects_matching '**/Shantyfile'
end
end
27 changes: 23 additions & 4 deletions lib/shanty/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,42 @@
require 'attr_combined_accessor'
require 'call_me_ruby'

require 'shanty/env'

module Shanty
# Public: Represents a project in the current repository.
class Project
include ActsAsGraphVertex
include CallMeRuby
include Env

attr_combined_accessor :name, :tags, :options
attr_accessor :path, :parents_by_path

# Multiton or Flyweight pattern - only allow once instance per unique path.
#
# See https://en.wikipedia.org/wiki/Multiton_pattern, http://en.wikipedia.org/wiki/Flyweight_pattern, or
# http://blog.rubybestpractices.com/posts/gregory/059-issue-25-creational-design-patterns.html for more information.
#
# Note that this is _not_ currently threadsafe.
class << self
alias_method :__new__, :new

def new(path)
(@instances ||= {})[path] ||= __new__(path)
end

def clear!
@instances = {}
end
end

# Public: Initialise the Project instance.
#
# env - The environment, an instance of Env.
# path - The path to the project.
def initialize(env, path)
def initialize(path)
fail('Path to project must be a directory.') unless File.directory?(path)

@env = env
@path = path

@name = File.basename(path)
Expand All @@ -39,7 +58,7 @@ def plugin(plugin)

def parent(parent)
# Will make the parent path absolute to the root if (and only if) it is relative.
@parents_by_path << File.expand_path(parent, @env.root)
@parents_by_path << File.expand_path(parent, root)
end

def tag(tag)
Expand Down
26 changes: 10 additions & 16 deletions lib/shanty/task_env.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
require 'delegate'

require 'shanty/plugin'
require 'shanty/project'
require 'shanty/project_linker'

module Shanty
#
class TaskEnv < SimpleDelegator
alias_method :env, :__getobj__
module TaskEnv
# Idiom to allow singletons that can be mixed in: http://ozmm.org/posts/singin_singletons.html
extend self

def graph
@graph ||= ProjectLinker.new(projects).link.tap do |graph|
Plugin.with_graph(env, graph)
end
def clear!
@graph = nil
end

private
def graph
return @graph unless @graph.nil?

def projects
Plugin.discover_all_projects(env).map do |path, plugins|
Project.new(env, path).tap do |project|
plugins.each { |plugin| project.plugin(plugin) }
project.execute_shantyfile!
end
projects = Plugin.discover_all_projects.each(&:execute_shantyfile!)
@graph ||= ProjectLinker.new(projects).link.tap do |graph|
Plugin.with_graph(graph)
end
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/shanty/task_set.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
require 'shanty/env'
require 'shanty/task_env'

module Shanty
# Public: Discover shanty tasks
class TaskSet
attr_reader :task_env

def initialize(task_env)
@task_env = task_env
end
include Env
include TaskEnv

# This method is auto-triggered by Ruby whenever a class inherits from
# Shanty::TaskSet. This means we can build up a list of all the tasks
Expand Down

0 comments on commit 5be04c1

Please sign in to comment.