pico 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|