pico 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.vimrc +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +33 -0
- data/gold_master_apps/tic_tac_toe/Gemfile +18 -0
- data/gold_master_apps/tic_tac_toe/Rakefile +3 -0
- data/gold_master_apps/tic_tac_toe/boot.rb +2 -0
- data/gold_master_apps/tic_tac_toe/config/application.rb +8 -0
- data/gold_master_apps/tic_tac_toe/config/environments/test.rb +3 -0
- data/gold_master_apps/tic_tac_toe/game/bad_move.rb +1 -0
- data/gold_master_apps/tic_tac_toe/game/game.rb +62 -0
- data/gold_master_apps/tic_tac_toe/game/make_move_command.rb +38 -0
- data/gold_master_apps/tic_tac_toe/game/memory_repository.rb +39 -0
- data/gold_master_apps/tic_tac_toe/test/game_test.rb +65 -0
- data/gold_master_apps/tic_tac_toe/test/reports/TEST-GameTest.xml +13 -0
- data/gold_master_apps/tic_tac_toe/test/test_helper.rb +7 -0
- data/lib/pico.rb +41 -0
- data/lib/pico/application.rb +120 -0
- data/lib/pico/autoloader.rb +131 -0
- data/lib/pico/context.rb +89 -0
- data/lib/pico/context/const_resolver.rb +80 -0
- data/lib/pico/pry_context.rb +71 -0
- data/lib/pico/rake.rb +15 -0
- data/lib/pico/ruse_extensions.rb +9 -0
- data/lib/pico/string_inflections.rb +30 -0
- data/lib/pico/test_runner.rb +27 -0
- data/lib/pico/version.rb +3 -0
- data/pico.gemspec +30 -0
- data/test/integration/autoloading_test.rb +47 -0
- data/test/support/fakes_filesystem.rb +182 -0
- data/test/support/tests_autoloading.rb +16 -0
- data/test/test_helper.rb +36 -0
- data/test/unit/application_test.rb +79 -0
- data/test/unit/autoloader_test.rb +68 -0
- data/test/unit/context_test.rb +80 -0
- metadata +202 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
module Pico
|
2
|
+
class Application < Context
|
3
|
+
attr :autoload_paths, :injector
|
4
|
+
|
5
|
+
def initialize(name, root: nil, **params, &config_block)
|
6
|
+
super name, root: root, **params
|
7
|
+
@autoload_paths = []
|
8
|
+
@injector = build_injector
|
9
|
+
configure(config_block) if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure(config_block)
|
13
|
+
ConfigurationContext.evaluate(config_block, application: self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def boot!
|
17
|
+
self.root ||= self.class.default_root
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_const_resolver(expanded_const)
|
22
|
+
resolver = super
|
23
|
+
resolver.autoload_paths = autoload_paths
|
24
|
+
resolver
|
25
|
+
end
|
26
|
+
|
27
|
+
def possible_implicit_namespace?(path)
|
28
|
+
autoload_paths.any? do |autoload_path|
|
29
|
+
root.join(autoload_path, path).directory?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def root=(dir)
|
34
|
+
raise ArgumentError, "Cannot alter root after booting" if booted?
|
35
|
+
@root = Pathname(dir)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_injector
|
41
|
+
injector = Ruse.create_injector
|
42
|
+
injector.pico_context = self
|
43
|
+
injector
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_mod
|
47
|
+
ApplicationModule.new self
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
def default_root
|
52
|
+
caller_locations.reduce do |previous_location, location|
|
53
|
+
if previous_location.label == "boot!" && File.basename(location.path) != "pico.rb"
|
54
|
+
return Pathname(location.absolute_path).dirname
|
55
|
+
end
|
56
|
+
location
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ApplicationModule < Module
|
62
|
+
attr :application
|
63
|
+
|
64
|
+
def initialize(application)
|
65
|
+
@application = application
|
66
|
+
extend self
|
67
|
+
end
|
68
|
+
|
69
|
+
def build(const_name, **params)
|
70
|
+
child_injector = application.injector.clone
|
71
|
+
child_injector.configure values: params
|
72
|
+
child_injector.get const_name
|
73
|
+
rescue Ruse::UnknownServiceError => ruse_error
|
74
|
+
raise Exception, "could not resolve dependency `#{ruse_error}'"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class ConfigurationContext
|
79
|
+
def initialize(application)
|
80
|
+
define_singleton_method :__application__ do
|
81
|
+
application
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.evaluate(config_block, application:)
|
86
|
+
new(application).instance_eval &config_block
|
87
|
+
end
|
88
|
+
|
89
|
+
def autoload_paths
|
90
|
+
__application__.autoload_paths
|
91
|
+
end
|
92
|
+
|
93
|
+
def autoload_paths=(paths)
|
94
|
+
autoload_paths.clear
|
95
|
+
autoload_paths.concat paths
|
96
|
+
end
|
97
|
+
|
98
|
+
def provide(provision = nil, as:, &factory_block)
|
99
|
+
if block_given?
|
100
|
+
raise ArgumentError, "cannot supply a block and a value" if provision
|
101
|
+
provision = factory_block
|
102
|
+
end
|
103
|
+
config_key, value = extract_ruse_config provision
|
104
|
+
__application__.injector.configure config_key => { as.to_sym => value }
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def extract_ruse_config(provision)
|
110
|
+
case provision
|
111
|
+
when Class then [:aliases, provision.name]
|
112
|
+
when String then [:aliases, provision]
|
113
|
+
when -> p { p.respond_to?(:call) } then [:factories, provision]
|
114
|
+
else [:values, provision]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Pico
|
2
|
+
using Pico::StringInflections
|
3
|
+
|
4
|
+
class Autoloader
|
5
|
+
attr :base_nibbles, :context, :from, :dir_paths
|
6
|
+
|
7
|
+
def initialize(from)
|
8
|
+
@from = from
|
9
|
+
@context, @base_nibbles = fetch_context_and_base_nibbles
|
10
|
+
@dir_paths = [nil]
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(const_name)
|
14
|
+
raise_name_error!(const_name) unless context
|
15
|
+
@dir_paths = each_possible_const(const_name).reduce [] do |new_paths, possible_const|
|
16
|
+
resolved_const = context.resolve_const possible_const
|
17
|
+
throw :const, resolved_const if resolved_const
|
18
|
+
new_paths << possible_const if dir?(possible_const)
|
19
|
+
new_paths
|
20
|
+
end
|
21
|
+
raise_name_error!(const_name) if dir_paths.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def each_possible_const(const_name)
|
25
|
+
return to_enum(:each_possible_const, const_name) unless block_given?
|
26
|
+
dir_paths.each do |dir_path|
|
27
|
+
base_nibbles.map { |e| yield constify(e, *dir_path, const_name) }
|
28
|
+
yield constify(*dir_path, const_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def dir?(possible_const)
|
33
|
+
context.possible_implicit_namespace? possible_const.to_snake_case
|
34
|
+
end
|
35
|
+
|
36
|
+
def constify(*nibbles)
|
37
|
+
nibbles.compact.join '::'
|
38
|
+
end
|
39
|
+
|
40
|
+
def raise_name_error!(const_name = dir_paths.last)
|
41
|
+
message = "uninitialized constant #{const_name}"
|
42
|
+
message << " (in #{context.mod})" if context
|
43
|
+
raise NameError, message
|
44
|
+
end
|
45
|
+
|
46
|
+
# given "Foo::Bar::Baz", return ["Foo::Bar::Baz", "Foo::Bar", etc.]
|
47
|
+
def fetch_context_and_base_nibbles
|
48
|
+
each_base_nibble.to_a.reverse.reduce [] do |ary, (mod, const)|
|
49
|
+
owner = Pico.contexts.each_value.detect { |c| c.mod == mod }
|
50
|
+
return [owner, ary] if owner
|
51
|
+
ary.map! do |e| e.insert 0, '::'; e.insert 0, const; end
|
52
|
+
ary << const
|
53
|
+
end
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# given "Foo::Bar::Baz", return ["Foo", "Bar", "Baz"]
|
58
|
+
def each_base_nibble
|
59
|
+
return to_enum(:each_base_nibble) unless block_given?
|
60
|
+
from.name.split('::').reduce Object do |mod, const|
|
61
|
+
mod = mod.const_get const
|
62
|
+
yield [mod, const]
|
63
|
+
mod
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module ThreadedState
|
68
|
+
def autoloaders
|
69
|
+
@autoloaders ||= {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_autoloader
|
73
|
+
autoloaders[current_thread_id]
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_current_autoloader(to:)
|
77
|
+
autoloaders[current_thread_id] = to
|
78
|
+
end
|
79
|
+
|
80
|
+
def current_thread_id
|
81
|
+
Thread.current.object_id
|
82
|
+
end
|
83
|
+
end
|
84
|
+
extend ThreadedState
|
85
|
+
|
86
|
+
module HandleConstMissing
|
87
|
+
def handle(*args)
|
88
|
+
found_const = catch :const do
|
89
|
+
handle! *args and return NullModule
|
90
|
+
end
|
91
|
+
throw :const, found_const
|
92
|
+
rescue NameError => name_error; raise name_error
|
93
|
+
ensure
|
94
|
+
set_current_autoloader(to: nil) if found_const or name_error
|
95
|
+
end
|
96
|
+
|
97
|
+
def handle!(const_name, from:)
|
98
|
+
autoloader = current_autoloader || new(from)
|
99
|
+
autoloader << String(const_name)
|
100
|
+
set_current_autoloader to: autoloader
|
101
|
+
end
|
102
|
+
end
|
103
|
+
extend HandleConstMissing
|
104
|
+
|
105
|
+
module NullModule
|
106
|
+
extend self
|
107
|
+
|
108
|
+
def method_missing(*)
|
109
|
+
Autoloader.current_autoloader.raise_name_error!
|
110
|
+
ensure
|
111
|
+
Autoloader.set_current_autoloader to: nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Module
|
118
|
+
def const_missing(const)
|
119
|
+
catch :const do
|
120
|
+
Pico::Autoloader.handle const, from: self
|
121
|
+
end
|
122
|
+
rescue NameError => name_error
|
123
|
+
if name_error.class == NameError
|
124
|
+
# Reraise the error to keep our junk out of the backtrace
|
125
|
+
raise NameError, name_error.message
|
126
|
+
else
|
127
|
+
# NoMethodError inherits from NameError
|
128
|
+
raise name_error
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
data/lib/pico/context.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "pico/context/const_resolver"
|
2
|
+
|
3
|
+
module Pico
|
4
|
+
class Context
|
5
|
+
attr :mod, :name, :root
|
6
|
+
|
7
|
+
def initialize(name, parent: nil, root:)
|
8
|
+
@mod = build_mod
|
9
|
+
@name = name
|
10
|
+
@root = Pathname(root) if root
|
11
|
+
@parent = parent
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def owner(const)
|
16
|
+
owner, _ = Autoloader.owner_and_ascending_nibbles const
|
17
|
+
owner
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_mod
|
22
|
+
Module.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def boot!
|
26
|
+
parent.const_set name, mod
|
27
|
+
end
|
28
|
+
|
29
|
+
def booted?
|
30
|
+
parent.const_defined? name
|
31
|
+
end
|
32
|
+
|
33
|
+
def eager_load!
|
34
|
+
Dir[root.join('**/*.rb')].each do |rb_file|
|
35
|
+
load_file rb_file
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_file(rb_file)
|
40
|
+
mod.module_eval File.read(rb_file), rb_file.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent
|
44
|
+
return Object unless @parent
|
45
|
+
Pico.contexts.fetch(@parent.to_sym).mod
|
46
|
+
end
|
47
|
+
|
48
|
+
def reload!
|
49
|
+
shutdown!
|
50
|
+
@mod = build_mod
|
51
|
+
boot!
|
52
|
+
end
|
53
|
+
|
54
|
+
def possible_implicit_namespace?(path)
|
55
|
+
root.join(path).directory?
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolve_const(expanded_const)
|
59
|
+
build_const_resolver(expanded_const).resolve
|
60
|
+
end
|
61
|
+
|
62
|
+
def shutdown!
|
63
|
+
parent.send(:remove_const, name)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def build_const_resolver(expanded_const)
|
69
|
+
ConstResolver.new(
|
70
|
+
context: self,
|
71
|
+
expanded_const: String(expanded_const).dup.freeze,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
class AutoloadError < NameError
|
78
|
+
attr :const, :rb_file
|
79
|
+
|
80
|
+
def initialize(const:, rb_file:)
|
81
|
+
@const = const
|
82
|
+
@rb_file = rb_file
|
83
|
+
end
|
84
|
+
|
85
|
+
def message
|
86
|
+
"Expected #{rb_file} to define #{const}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Pico
|
2
|
+
using StringInflections
|
3
|
+
|
4
|
+
class Context
|
5
|
+
class ConstResolver
|
6
|
+
attr :current_path, :autoload_paths, :context, :expanded_const
|
7
|
+
|
8
|
+
def initialize(context:, expanded_const:, autoload_paths: [''])
|
9
|
+
@autoload_paths = autoload_paths
|
10
|
+
@context = context
|
11
|
+
@expanded_const = expanded_const
|
12
|
+
end
|
13
|
+
|
14
|
+
def autoload_paths=(paths)
|
15
|
+
autoload_paths.clear
|
16
|
+
autoload_paths.concat paths
|
17
|
+
end
|
18
|
+
|
19
|
+
def const_get
|
20
|
+
walk_const_parts.reduce context.mod do |mod, const_part|
|
21
|
+
return nil unless mod.const_defined?(const_part)
|
22
|
+
mod.const_get const_part
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_autoload_path
|
27
|
+
autoload_paths.each do |autoload_path|
|
28
|
+
@current_path = context.root.join autoload_path
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
@current_path = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def each_possible_rb_file
|
35
|
+
each_autoload_path do
|
36
|
+
base_path = current_path.join expanded_const.to_snake_case
|
37
|
+
each_rb_file_from_base_path base_path do |rb_file|
|
38
|
+
yield rb_file if File.exist?(rb_file)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def each_rb_file_from_base_path(base_path)
|
44
|
+
base_path.ascend do |path|
|
45
|
+
return if path == context.root
|
46
|
+
rb_file = path.sub_ext '.rb'
|
47
|
+
yield rb_file if rb_file.file?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def resolve
|
52
|
+
each_possible_rb_file do |rb_file|
|
53
|
+
context.load_file rb_file
|
54
|
+
check_loaded rb_file
|
55
|
+
end
|
56
|
+
const_get
|
57
|
+
end
|
58
|
+
|
59
|
+
def walk_const_parts(const = expanded_const)
|
60
|
+
return to_enum(:walk_const_parts, const) if block_given?
|
61
|
+
const.split '::'
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_loaded(rb_file)
|
65
|
+
expected_const = expected_const_defined_in_rb_file rb_file
|
66
|
+
walk_const_parts(expected_const).reduce context.mod do |mod, const_part|
|
67
|
+
mod.const_defined?(const_part) or
|
68
|
+
raise AutoloadError.new(const: expected_const, rb_file: rb_file)
|
69
|
+
mod.const_get const_part
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def expected_const_defined_in_rb_file(rb_file, autoload_path: current_path)
|
74
|
+
rel_path = rb_file.sub_ext('').relative_path_from(autoload_path).to_s
|
75
|
+
matcher = %r{\A(#{rel_path.to_camel_case})}i
|
76
|
+
expanded_const.match(matcher).captures.fetch 0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'pry'
|
2
|
+
|
3
|
+
require "pico/test_runner"
|
4
|
+
|
5
|
+
module Pico
|
6
|
+
module PryContext
|
7
|
+
extend self
|
8
|
+
|
9
|
+
attr :commands_defined
|
10
|
+
|
11
|
+
def start!
|
12
|
+
if Pico.application.booted?
|
13
|
+
raise Error, "cannot start pry context if application has booted"
|
14
|
+
end
|
15
|
+
define_commands unless commands_defined
|
16
|
+
Pico.boot!
|
17
|
+
Pico.application.mod.pry
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_commands
|
21
|
+
define_reload_command
|
22
|
+
define_rake_command
|
23
|
+
define_tmux_command if ENV['TMUX']
|
24
|
+
@commands_defined = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_reload_command
|
28
|
+
Pry::Commands.block_command "reload!", "Reload #{Pico.application.name}" do
|
29
|
+
puts "Reloading #{Pico.application.name}..."
|
30
|
+
Pico.application.reload!
|
31
|
+
_pry_.binding_stack.push Pico.application.mod.__binding__
|
32
|
+
_pry_.binding_stack.shift until _pry_.binding_stack.size == 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def define_rake_command
|
37
|
+
Pry::Commands.create_command "rake", keep_retval: true do
|
38
|
+
description "Run the test suite"
|
39
|
+
|
40
|
+
def process
|
41
|
+
build_passed = Pico::TestRunner.run!
|
42
|
+
operator = args.shift
|
43
|
+
command = args.shift
|
44
|
+
evaluate_hook(build_passed, operator, command, args) if operator
|
45
|
+
build_passed
|
46
|
+
end
|
47
|
+
|
48
|
+
def evaluate_hook(build_passed, operator, command, args)
|
49
|
+
if operator == '&&'
|
50
|
+
run(command, args) if build_passed
|
51
|
+
elsif operator == '||'
|
52
|
+
run(command, args) unless build_passed
|
53
|
+
else
|
54
|
+
raise ArgumentError, "Must supply either '&&' or '||' operators, followed by a pry command"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_tmux_command
|
60
|
+
Pry::Commands.create_command "tmux" do
|
61
|
+
description "Run a tmux command"
|
62
|
+
|
63
|
+
def process
|
64
|
+
system 'tmux', *args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|