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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.vimrc +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +29 -0
  7. data/Rakefile +33 -0
  8. data/gold_master_apps/tic_tac_toe/Gemfile +18 -0
  9. data/gold_master_apps/tic_tac_toe/Rakefile +3 -0
  10. data/gold_master_apps/tic_tac_toe/boot.rb +2 -0
  11. data/gold_master_apps/tic_tac_toe/config/application.rb +8 -0
  12. data/gold_master_apps/tic_tac_toe/config/environments/test.rb +3 -0
  13. data/gold_master_apps/tic_tac_toe/game/bad_move.rb +1 -0
  14. data/gold_master_apps/tic_tac_toe/game/game.rb +62 -0
  15. data/gold_master_apps/tic_tac_toe/game/make_move_command.rb +38 -0
  16. data/gold_master_apps/tic_tac_toe/game/memory_repository.rb +39 -0
  17. data/gold_master_apps/tic_tac_toe/test/game_test.rb +65 -0
  18. data/gold_master_apps/tic_tac_toe/test/reports/TEST-GameTest.xml +13 -0
  19. data/gold_master_apps/tic_tac_toe/test/test_helper.rb +7 -0
  20. data/lib/pico.rb +41 -0
  21. data/lib/pico/application.rb +120 -0
  22. data/lib/pico/autoloader.rb +131 -0
  23. data/lib/pico/context.rb +89 -0
  24. data/lib/pico/context/const_resolver.rb +80 -0
  25. data/lib/pico/pry_context.rb +71 -0
  26. data/lib/pico/rake.rb +15 -0
  27. data/lib/pico/ruse_extensions.rb +9 -0
  28. data/lib/pico/string_inflections.rb +30 -0
  29. data/lib/pico/test_runner.rb +27 -0
  30. data/lib/pico/version.rb +3 -0
  31. data/pico.gemspec +30 -0
  32. data/test/integration/autoloading_test.rb +47 -0
  33. data/test/support/fakes_filesystem.rb +182 -0
  34. data/test/support/tests_autoloading.rb +16 -0
  35. data/test/test_helper.rb +36 -0
  36. data/test/unit/application_test.rb +79 -0
  37. data/test/unit/autoloader_test.rb +68 -0
  38. data/test/unit/context_test.rb +80 -0
  39. 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
@@ -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