diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index fb57d1b..527f47c 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -16,7 +16,7 @@ $ bundle add bake - A `bake` executable used for invoking one or more tasks. - A {ruby Bake::Context} instance which is bound to a project or gem and exposes a hierarchy of runnable tasks. -- A {ruby Bake::Loaders} instance which is used for on-demand loading of bake files from the current project and all available gems. +- A {ruby Bake::Loader::Aggregate} instance which is used for on-demand loading of bake files from the current project and all available gems. ## Executing Tasks diff --git a/lib/bake.rb b/lib/bake.rb index f8bd60f..5976adc 100644 --- a/lib/bake.rb +++ b/lib/bake.rb @@ -4,6 +4,5 @@ # Copyright, 2020-2024, by Samuel Williams. require_relative 'bake/version' -require_relative 'bake/loaders' -require_relative 'bake/loader' +require_relative 'bake/registry' require_relative 'bake/context' diff --git a/lib/bake/command/call.rb b/lib/bake/command/call.rb index 79bfb33..b641cfe 100644 --- a/lib/bake/command/call.rb +++ b/lib/bake/command/call.rb @@ -5,8 +5,7 @@ require 'samovar' -require_relative '../loaders' -require_relative '../loader' +require_relative '../registry' require_relative '../context' module Bake diff --git a/lib/bake/command/list.rb b/lib/bake/command/list.rb index 1841797..7ae70a9 100644 --- a/lib/bake/command/list.rb +++ b/lib/bake/command/list.rb @@ -80,21 +80,11 @@ def call terminal = @parent.terminal context = @parent.context - if scope = context.scope - printed = print_scope(terminal, context.scope) do - terminal.print_line(:context, context) - end - - if printed - terminal.print_line - end - end - - context.loaders.each do |loader| + context.registry.each do |loader| printed = false loader.each do |path| - if scope = loader.scope_for(path) + loader.scopes_for(path) do |scope| print_scope(terminal, scope, printed: printed) do terminal.print_line(:loader, loader) printed = true diff --git a/lib/bake/context.rb b/lib/bake/context.rb index 6376ff5..36043ed 100644 --- a/lib/bake/context.rb +++ b/lib/bake/context.rb @@ -45,53 +45,34 @@ def self.bakefile_path(path, bakefile: BAKEFILE) # @path [String] A file-system path. def self.load(path = Dir.pwd) if bakefile_path = self.bakefile_path(path) - scope = Scope.load(bakefile_path) - working_directory = File.dirname(bakefile_path) - loaders = Loaders.default(working_directory) else - scope = nil - working_directory = path - loaders = Loaders.default(working_directory) end - return self.new(loaders, scope, working_directory) + registry = Registry.default(working_directory, bakefile_path) + instance = self.new(registry, working_directory) + + return instance end - # Initialize the context with the specified loaders. - # @parameter loaders [Loaders] - def initialize(loaders, scope = nil, root = nil) - @loaders = loaders - - @wrappers = Hash.new do |hash, key| - hash[key] = [] - end + # Initialize the context with the specified registry. + # @parameter registry [Registry] + def initialize(registry, root = nil) + @registry = registry + @root = root @instances = Hash.new do |hash, key| hash[key] = instance_for(key) end - @scope = scope - @root = root - - if @scope - base = Base.derive - base.prepend(@scope) - - @instances[[]] = base.new(self) - end - @recipes = Hash.new do |hash, key| hash[key] = recipe_for(key) end end - # The loaders which will be used to resolve recipes in this context. - attr :loaders - - # The scope for the root {BAKEFILE}. - attr :scope + # The registry which will be used to resolve recipes in this context. + attr :registry # The root path of this context. # @returns [String | Nil] @@ -123,35 +104,8 @@ def lookup(command) @recipes[command] end - class Wrapper - def initialize(wrappers, path) - @wrappers = wrappers - @path = path - end - - def before(name = @path.last, &block) - wrapper = Module.new - wrapper.define_method(name) do |*arguments, **options| - instance_exec(&block) - super(*arguments, **options) - end - - @wrappers[@path] << wrapper - end - - def after(name = @path.last, &block) - wrapper = Module.new - wrapper.define_method(name) do |*arguments, **options| - super(*arguments, **options) - instance_exec(&block) - end - - @wrappers[@path] << wrapper - end - end - - def wrap(*path, &block) - Wrapper.new(@wrappers, path).instance_exec(&block) + def wrap(...) + @registry.wrap(...) end def to_s @@ -199,17 +153,9 @@ def base_for(path) base = nil # For each loader, we check if it has a scope for the given path. If it does, we prepend it to the base: - @loaders.each do |loader| - if scope = loader.scope_for(path) - base ||= Base.derive(path) - - base.prepend(scope) - end - end - - # If we have any wrappers for the given path, we also prepend them to the base: - @wrappers[path].each do |wrapper| - base.prepend(wrapper) + @registry.scopes_for(path) do |scope| + base ||= Base.derive(path) + base.prepend(scope) end return base diff --git a/lib/bake/loader.rb b/lib/bake/loader.rb deleted file mode 100644 index 1a0cd66..0000000 --- a/lib/bake/loader.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020-2024, by Samuel Williams. - -require_relative 'scope' - -module Bake - # Represents a directory which contains bakefiles. - class Loader - # Initialize the loader with the specified root path. - # @parameter root [String] A file-system path. - def initialize(root, name: nil) - @root = root - @name = name - end - - def to_s - "#{self.class} #{@name || @root}" - end - - # The root path for this loader. - attr :root - - # Enumerate all bakefiles within the loaders root directory. - # @yields {|path| ...} - # @parameter path [String] The Ruby source file path. - def each - return to_enum unless block_given? - - Dir.glob("**/*.rb", base: @root) do |file_path| - yield file_path.sub(/\.rb$/, '').split(File::SEPARATOR) - end - end - - # Load the {Scope} for the specified relative path within this loader, if it exists. - # @parameter path [Array(String)] A relative path. - def scope_for(path) - *directory, file = *path - - file_path = File.join(@root, directory, "#{file}.rb") - - if File.exist?(file_path) - return Scope.load(file_path, path) - end - end - end -end diff --git a/lib/bake/loaders.rb b/lib/bake/loaders.rb deleted file mode 100644 index b541411..0000000 --- a/lib/bake/loaders.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020-2024, by Samuel Williams. - -require 'console' - -require_relative 'loader' - -module Bake - # Structured access to the working directory and loaded gems for loading bakefiles. - class Loaders - include Enumerable - - # Create a loader using the specified working directory. - # @parameter working_directory [String] - def self.default(working_directory) - loaders = self.new - - loaders.append_defaults(working_directory) - - return loaders - end - - # Initialize an empty array of loaders. - def initialize - @roots = {} - @ordered = Array.new - end - - # Whether any loaders are defined. - # @returns [Boolean] - def empty? - @ordered.empty? - end - - # Add loaders according to the current working directory and loaded gems. - # @parameter working_directory [String] - def append_defaults(working_directory) - # Load recipes from working directory: - self.append_path(working_directory) - - # Load recipes from loaded gems: - self.append_from_gems - end - - # Enumerate the loaders in order. - def each(&block) - @ordered.each(&block) - end - - # Append a specific project path to the search path for recipes. - # The computed path will have `bake` appended to it. - # @parameter current [String] The path to add. - def append_path(current = Dir.pwd, **options) - bake_path = File.join(current, "bake") - - if File.directory?(bake_path) - return insert(bake_path, **options) - end - - return false - end - - # Search from the current working directory until a suitable bakefile is found and add it. - # @parameter current [String] The path to start searching from. - def append_from_root(current = Dir.pwd, **options) - while current - Console.logger.debug(self) {"Checking current #{current}..."} - - append_path(current, **options) - - parent = File.dirname(current) - - if current == parent - break - else - current = parent - end - end - end - - # Enumerate all loaded gems and add them. - def append_from_gems - ::Gem.loaded_specs.each do |name, spec| - Console.logger.debug(self) {"Checking gem #{name}: #{spec.full_gem_path}..."} - - if path = spec.full_gem_path and File.directory?(path) - append_path(path, name: spec.full_name) - end - end - end - - protected - - def insert(directory, **options) - unless @roots.key?(directory) - Console.logger.debug(self) {"Adding #{directory.inspect}"} - - loader = Loader.new(directory, **options) - @roots[directory] = loader - @ordered << loader - - return true - end - - return false - end - end -end diff --git a/lib/bake/registry.rb b/lib/bake/registry.rb new file mode 100644 index 0000000..45560cf --- /dev/null +++ b/lib/bake/registry.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require_relative 'registry/aggregate' + +module Bake + # Structured access to the working directory and loaded gems for loading bakefiles. + module Registry + def self.default(...) + Aggregate.default(...) + end + end +end diff --git a/lib/bake/registry/aggregate.rb b/lib/bake/registry/aggregate.rb new file mode 100644 index 0000000..72a6b05 --- /dev/null +++ b/lib/bake/registry/aggregate.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require 'console' + +require_relative 'directory_loader' +require_relative 'file_loader' +require_relative 'wrappers' + +module Bake + # Structured access to the working directory and loaded gems for loading bakefiles. + module Registry + class Aggregate + include Enumerable + + # Create a loader using the specified working directory. + # @parameter working_directory [String] + def self.default(working_directory, bakefile_path = nil) + registry = self.new + + if bakefile_path + registry.append_bakefile(bakefile_path) + end + + registry.append_defaults(working_directory) + + return registry + end + + # Initialize an empty array of registry. + def initialize + # Used to de-duplicated directories: + @roots = {} + + # The ordered list of loaders: + @ordered = Array.new + + @wrappers = Wrappers.new + end + + def wrap(...) + @wrappers.wrap(...) + end + + # Whether any registry are defined. + # @returns [Boolean] + def empty? + @ordered.empty? + end + + # Enumerate the registry in order. + def each(&block) + @ordered.each(&block) + yield @wrappers + end + + def scopes_for(path, &block) + @ordered.each do |registry| + registry.scopes_for(path, &block) + end + + @wrappers.scopes_for(path, &block) + end + + def append_bakefile(path) + @ordered << FileLoader.new({ + [] => path + }) + end + + # Append a specific project path to the search path for recipes. + # The computed path will have `bake` appended to it. + # @parameter current [String] The path to add. + def append_path(current = Dir.pwd, **options) + bake_path = File.join(current, "bake") + + if File.directory?(bake_path) + return insert(bake_path, **options) + end + + return false + end + + # Add registry according to the current working directory and loaded gems. + # @parameter working_directory [String] + def append_defaults(working_directory) + # Load recipes from working directory: + self.append_path(working_directory) + + # Load recipes from loaded gems: + self.append_from_gems + end + + # Enumerate all loaded gems and add them. + def append_from_gems + ::Gem.loaded_specs.each do |name, spec| + Console.debug(self) {"Checking gem #{name}: #{spec.full_gem_path}..."} + + if path = spec.full_gem_path and File.directory?(path) + append_path(path, name: spec.full_name) + end + end + end + + protected + + def insert(directory, **options) + unless @roots.key?(directory) + Console.debug(self) {"Adding #{directory.inspect}"} + + loader = DirectoryLoader.new(directory, **options) + @roots[directory] = loader + @ordered << loader + + return true + end + + return false + end + end + end +end diff --git a/lib/bake/registry/directory_loader.rb b/lib/bake/registry/directory_loader.rb new file mode 100644 index 0000000..eaa6134 --- /dev/null +++ b/lib/bake/registry/directory_loader.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require_relative '../scope' + +module Bake + module Registry + # Represents a directory which contains bakefiles. + class DirectoryLoader + # Initialize the loader with the specified root path. + # @parameter root [String] A file-system path. + def initialize(root, name: nil) + @root = root + @name = name + end + + def to_s + "#{self.class} #{@name || @root}" + end + + # The root path for this loader. + attr :root + + # Enumerate all bakefiles within the loaders root directory. + # + # You can pass the yielded path to {scope_for} to load the corresponding {Scope}. + # + # @yields {|path| ...} + # @parameter path [String] The (relative) scope path. + def each + return to_enum unless block_given? + + Dir.glob("**/*.rb", base: @root) do |file_path| + yield file_path.sub(/\.rb$/, '').split(File::SEPARATOR) + end + end + + # Load the {Scope} for the specified relative path within this loader, if it exists. + # @parameter path [Array(String)] A relative path. + def scopes_for(path) + *directory, file = *path + + file_path = File.join(@root, directory, "#{file}.rb") + + if File.exist?(file_path) + yield Scope.load(file_path, path) + end + end + end + + class FileLoader + def initialize(paths) + @paths = paths + end + + def to_s + "#{self.class} #{@paths}" + end + + attr :paths + + def each(&block) + @paths.each_key(&block) + end + + def scope_for(path) + if file_path = @paths[path] + if File.exist?(file_path) + return Scope.load(file_path, path) + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/bake/registry/file_loader.rb b/lib/bake/registry/file_loader.rb new file mode 100644 index 0000000..27ec173 --- /dev/null +++ b/lib/bake/registry/file_loader.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require_relative '../scope' + +module Bake + module Registry + class FileLoader + def initialize(paths) + @paths = paths + end + + def to_s + "#{self.class} #{@paths}" + end + + attr :paths + + def each(&block) + @paths.each_key(&block) + end + + def scopes_for(path) + if file_path = @paths[path] + if File.exist?(file_path) + yield Scope.load(file_path, path) + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/bake/registry/wrappers.rb b/lib/bake/registry/wrappers.rb new file mode 100644 index 0000000..52169cb --- /dev/null +++ b/lib/bake/registry/wrappers.rb @@ -0,0 +1,49 @@ +module Bake + module Registry + class Wrappers + def initialize + @wrappers = Hash.new do |hash, key| + hash[key] = [] + end + end + + class Wrapper + def initialize(wrappers, path) + @wrappers = wrappers + @path = path + end + + def before(name = @path.last, &block) + wrapper = Module.new + wrapper.define_method(name) do |*arguments, **options| + instance_exec(&block) + super(*arguments, **options) + end + + @wrappers[@path] << wrapper + end + + def after(name = @path.last, &block) + wrapper = Module.new + wrapper.define_method(name) do |*arguments, **options| + super(*arguments, **options) + instance_exec(&block) + end + + @wrappers[@path] << wrapper + end + end + + def wrap(*path, &block) + Wrapper.new(@wrappers, path).instance_exec(&block) + end + + def each(&block) + end + + def scopes_for(path, &block) + @wrappers[path].each(&block) + end + end + end +end diff --git a/lib/bake/scope.rb b/lib/bake/scope.rb index b040f86..d8cd652 100644 --- a/lib/bake/scope.rb +++ b/lib/bake/scope.rb @@ -41,6 +41,11 @@ def recipes end # The path of the file that was used to {load} this scope. + def file_path + self.const_get(:FILE_PATH) + end + + # The path of the scope, relative to the root of the context. def path self.const_get(:PATH) end diff --git a/test/bake/context.rb b/test/bake/context.rb index fb0075f..4b720ab 100644 --- a/test/bake/context.rb +++ b/test/bake/context.rb @@ -4,7 +4,6 @@ # Copyright, 2020-2024, by Samuel Williams. require 'bake/context' -require 'bake/loaders' describe Bake::Context do let(:bakefile) {File.expand_path(".test-project/bake.rb", __dir__)} @@ -72,5 +71,23 @@ expect(events).to be == [:before, :after] end + + it "can wrap unspecified methods" do + events = [] + + context.wrap('wrap') do + before do + events << :before + end + + after do + events << :after + end + end + + context.call('wrap') + + expect(events).to be == [:before, :after] + end end end diff --git a/test/bake/types.rb b/test/bake/types.rb index edaa79e..bcf08ec 100644 --- a/test/bake/types.rb +++ b/test/bake/types.rb @@ -5,7 +5,6 @@ require 'bake/types' require 'bake/context' -require 'bake/loaders' describe Bake::Types do it "can use | operator" do