pico 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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