Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ source "https://rubygems.org"

gemspec

gem "listen"

group :development do
gem "activesupport"
gem "dry-cli"
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ GEM
concurrent-ruby (1.1.9)
diff-lcs (1.4.4)
dry-cli (1.0.0)
ffi (1.15.5)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.4)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
minitest (5.14.4)
parallel (1.19.2)
parser (3.0.2.0)
ast (~> 2.4.1)
rainbow (3.0.0)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
Expand Down Expand Up @@ -66,6 +73,7 @@ DEPENDENCIES
awesome_print (~> 1.9.2)
dry-cli
fun-ruby!
listen
rake (~> 13.0.6)
rspec (~> 3.10.0)
rubocop (~> 0.81.0)
Expand Down
55 changes: 53 additions & 2 deletions lib/fun_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,48 @@ module FunRuby
# end
# end
# F.container.fetch("functions.sum").(2, 3) # => 5
def define(&block)
Container::Define.build(container: container).(&block)
def define(target = nil, &block)
target ||= container
file_path, _line_number = caller.first.split(":")
target.add_definition_path(file_path, true)
Container::Define.build(container: target).(&block)
end

# Accepts a glob of files where the container is defined and add the definition paths to the container
def add_definition_paths(glob, to: nil)
target = to || container
Dir.glob(glob).each do |path|
target.add_definition_path(path, false)
end
end

# Loads all the defined paths for the passed container
def load_definitions!(target = nil)
target ||= container
target.definition_paths.reject(&:loaded?).each do |definition|
load definition.path
end
end

# Enables hot reloading
# TODO: Create a better implementation
# TODO: Cover by tests
# TODO: Write a better documentation
# TODO: Check if a container support overriding
def enable_hot_reloading!(target = nil)
target ||= container

target.definition_paths.each do |definition|
definition_dir = begin
*dir_parts, _file = definition.path.split("/")
dir_parts.join("/")
end

listener = Listen.to(definition_dir) do |modified|
load definition.path if modified.include?(definition.path)
end
listener.start
end
end

# Returns a global container
Expand All @@ -35,6 +75,17 @@ def container
@container ||= Container.new
end

# Accepts a container that will be considered global
#
# @since 0.1.0
#
# @return [FunRuby::Container]
def container=(container)
raise ArgumentError, "The global container is already defined" if instance_variable_defined?(:@container)

@container = container
end

# Allows to import global container to your classes and modules
#
# @since 0.1.0
Expand Down
20 changes: 17 additions & 3 deletions lib/fun_ruby/container.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# frozen_string_literal: true

require_relative "container/mixin"
require_relative "container/config"
require_relative "container/definition_path"

module FunRuby
# @private
class Container
NAMESPACE_SEPARATOR = "."
NOT_EVALUATED = Object.new.freeze

# Returns a list of files with definitions for the container
attr_reader :definition_paths

# @private
def initialize
def initialize(config = Config.new)
@storage = {}
@mutex = Mutex.new
@config = config
@definition_paths = Set.new
end

# @private
def define(key, &block)
key = key.to_s

raise KeyError, "#{key.inspect} is already defined" if storage.key?(key)
raise KeyError, "#{key.inspect} is already defined" if storage.key?(key) && config.cant_override?
raise TypeError, "block should be given" unless block_given?

storage[key] = init_meta(block)
Expand All @@ -41,9 +48,16 @@ def import(*aliases)
Mixin.build(aliases: aliases)
end

# Adds a path to the definition path lit
def add_definition_path(path, loaded = false)
path = DefinitionPath.new(path: path, loaded: loaded)
# TODO: Add file existence validations and etc.
@definition_paths.add(path)
end

private

attr_reader :storage, :mutex
attr_reader :storage, :mutex, :config

def init_meta(definition)
{
Expand Down
27 changes: 27 additions & 0 deletions lib/fun_ruby/container/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module FunRuby
class Container
# The class store a configuration for a container
class Config
# @private
def initialize(override: false)
@override = override
end

# @private
def can_override?
@override
end

# @private
def cant_override?
!can_override?
end

private

attr_reader :override
end
end
end
40 changes: 40 additions & 0 deletions lib/fun_ruby/container/definition_path.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module FunRuby
class Container
# The class stores info about a container's definition path and its state
class DefinitionPath
# @private
attr_reader :path
# @private
def initialize(path:, loaded:)
@path = path
@loaded = loaded
end

# @private
def eql?(other)
path == other.path
end

# @private
def ==(other)
eql?(other) && loaded? == other.loaded?
end

# @private
def hash
path.hash
end

# @private
def loaded?
loaded
end

private

attr_reader :loaded
end
end
end
Empty file.
Empty file.
37 changes: 37 additions & 0 deletions spec/fun_ruby/container_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@

expect { container.define(key) { function } }.to raise_error(KeyError)
end

it "does not raise an error when overriding is allowed by config", :aggregate_failures do
config = described_class::Config.new(override: true)
container = described_class.new(config)
first_definition = ->(x, y) { x + y }
key = "sum"
second_definition = ->(a, b) { a - b }

container.define(key) { first_definition }

expect { container.define(key) { second_definition } }.not_to raise_error
expect(container.fetch(key)).to equal(second_definition)
end
end

describe "#fetch" do
Expand All @@ -42,4 +55,28 @@
expect { container.fetch(key) }.to raise_error(KeyError)
end
end

describe "#add_definition_path" do
it "stores a new definition paths as not loaded yet" do
container = described_class.new
container.add_definition_path(__FILE__)

definition_path = described_class::DefinitionPath.new(
path: __FILE__,
loaded: false
)
expect(container.definition_paths.first).to eq(definition_path)
end

it "allows storing definition paths as already loaded" do
container = described_class.new
container.add_definition_path(__FILE__, true)

definition_path = described_class::DefinitionPath.new(
path: __FILE__,
loaded: true
)
expect(container.definition_paths.first).to eq(definition_path)
end
end
end
91 changes: 91 additions & 0 deletions spec/fun_ruby_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require_relative "../lib/fun_ruby"

describe FunRuby do
before do
described_class.remove_instance_variable(:@container) if described_class.instance_variable_defined?(:@container)
end

describe ".container=" do
it "sets the passed container as global" do
container = described_class::Container.new

described_class.container = container

expect(described_class.container).to equal(container)
end

it "does not allow settings a global container twice" do
container = described_class::Container.new

described_class.container = container

expect { described_class.container = container }.to raise_error(ArgumentError)
end
end

describe ".define" do
it "defines for a global container where no args are passed" do
function = ->(x) { x }

described_class.define do
namespace :hello do
f(:world) { function }
end
end

expect(F.container.fetch("hello.world")).to equal(function)
end

it "defines for a passed container", :aggregate_failures do
container = described_class::Container.new
function = ->(x) { x }

described_class.define(container) do
namespace :hello do
f(:world) { function }
end
end

expect(container.fetch("hello.world")).to equal(function)
expect { F.container.fetch("hello.world") }.to raise_error(KeyError)
end

it "saves a definition path of the file where it's executed as already loaded" do
container = described_class::Container.new
function = ->(x) { x }

described_class.define(container) do
namespace :hello do
f(:world) { function }
end
end

definition_path = described_class::Container::DefinitionPath.new(
path: __FILE__,
loaded: true
)

expect(container.definition_paths.first).to eq(definition_path)
end
end

describe ".add_definition_paths" do
it "finds all the files matching the passed glob and stores them as not loaded definition paths" do
container = described_class::Container.new
glob = File.expand_path("./fixtures/container_definitions/**/*.rb", __dir__)

described_class.add_definition_paths(glob, to: container)

Dir.glob(glob).each do |file_path|
definition_path = container.definition_paths.find do |definition|
definition.path == file_path
end

expect(definition_path).not_to be(nil)
expect(definition_path.loaded?).to be(false)
end
end
end
end