From 7326eb11566c693feaa2c29abed1045c48ad1442 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 00:00:27 +0100 Subject: [PATCH 01/10] Allow containers to override definitions --- lib/fun_ruby/container.rb | 8 +++++--- lib/fun_ruby/container/config.rb | 27 +++++++++++++++++++++++++++ spec/fun_ruby/container_spec.rb | 13 +++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 lib/fun_ruby/container/config.rb diff --git a/lib/fun_ruby/container.rb b/lib/fun_ruby/container.rb index 8ec43f9..008cdb8 100644 --- a/lib/fun_ruby/container.rb +++ b/lib/fun_ruby/container.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "container/mixin" +require_relative "container/config" module FunRuby # @private @@ -9,16 +10,17 @@ class Container NOT_EVALUATED = Object.new.freeze # @private - def initialize + def initialize(config = Config.new) @storage = {} @mutex = Mutex.new + @config = config 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) @@ -43,7 +45,7 @@ def import(*aliases) private - attr_reader :storage, :mutex + attr_reader :storage, :mutex, :config def init_meta(definition) { diff --git a/lib/fun_ruby/container/config.rb b/lib/fun_ruby/container/config.rb new file mode 100644 index 0000000..c5669e2 --- /dev/null +++ b/lib/fun_ruby/container/config.rb @@ -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 diff --git a/spec/fun_ruby/container_spec.rb b/spec/fun_ruby/container_spec.rb index 59b8ddf..4cfb14a 100644 --- a/spec/fun_ruby/container_spec.rb +++ b/spec/fun_ruby/container_spec.rb @@ -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 From ce85681b97caef2ec465b0f79cad4ebba46f0db6 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 00:14:05 +0100 Subject: [PATCH 02/10] Add API to set up a global container --- lib/fun_ruby.rb | 11 +++++++++++ spec/fun_ruby_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 spec/fun_ruby_spec.rb diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index 78c3ed6..df11e65 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -35,6 +35,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 diff --git a/spec/fun_ruby_spec.rb b/spec/fun_ruby_spec.rb new file mode 100644 index 0000000..2a65ed5 --- /dev/null +++ b/spec/fun_ruby_spec.rb @@ -0,0 +1,27 @@ +# 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 +end From cd249eb37de273de13879ee80ebf8a9cc4931942 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 00:37:39 +0100 Subject: [PATCH 03/10] Add API to store container definitions --- lib/fun_ruby/container.rb | 10 ++++++++++ spec/fun_ruby/container/fixtures/definition_path.rb | 7 +++++++ spec/fun_ruby/container_spec.rb | 11 +++++++++++ 3 files changed, 28 insertions(+) create mode 100644 spec/fun_ruby/container/fixtures/definition_path.rb diff --git a/lib/fun_ruby/container.rb b/lib/fun_ruby/container.rb index 008cdb8..a7f9041 100644 --- a/lib/fun_ruby/container.rb +++ b/lib/fun_ruby/container.rb @@ -9,11 +9,15 @@ 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(config = Config.new) @storage = {} @mutex = Mutex.new @config = config + @definition_paths = Set.new end # @private @@ -43,6 +47,12 @@ def import(*aliases) Mixin.build(aliases: aliases) end + # Adds a path to the definition path lit + def add_definition_path(path) + # TODO: Add file existence validations and etc. + @definition_paths.add(path) + end + private attr_reader :storage, :mutex, :config diff --git a/spec/fun_ruby/container/fixtures/definition_path.rb b/spec/fun_ruby/container/fixtures/definition_path.rb new file mode 100644 index 0000000..6fc48eb --- /dev/null +++ b/spec/fun_ruby/container/fixtures/definition_path.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +TEST_CONTAINER.define do + namespace :hello do + f(:word) { -> { "Hello, world!" } } + end +end diff --git a/spec/fun_ruby/container_spec.rb b/spec/fun_ruby/container_spec.rb index 4cfb14a..26055b7 100644 --- a/spec/fun_ruby/container_spec.rb +++ b/spec/fun_ruby/container_spec.rb @@ -55,4 +55,15 @@ expect { container.fetch(key) }.to raise_error(KeyError) end end + + describe "#add_definition_path" do + it "stores a new definition paths" do + container = described_class.new + path = File.expand_path("./container/fixtures/definition_path.rb", __dir__) + binding.irb + container.add_definition_path(path) + + expect(container.definition_paths).to include(path) + end + end end From 9e3bea5cbccb4a48871b6395f0966b1d5115812f Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 00:46:35 +0100 Subject: [PATCH 04/10] Store container definitions --- spec/fun_ruby/container/fixtures/definition_path.rb | 7 ------- spec/fun_ruby/container_spec.rb | 6 ++---- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 spec/fun_ruby/container/fixtures/definition_path.rb diff --git a/spec/fun_ruby/container/fixtures/definition_path.rb b/spec/fun_ruby/container/fixtures/definition_path.rb deleted file mode 100644 index 6fc48eb..0000000 --- a/spec/fun_ruby/container/fixtures/definition_path.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -TEST_CONTAINER.define do - namespace :hello do - f(:word) { -> { "Hello, world!" } } - end -end diff --git a/spec/fun_ruby/container_spec.rb b/spec/fun_ruby/container_spec.rb index 26055b7..c1a2253 100644 --- a/spec/fun_ruby/container_spec.rb +++ b/spec/fun_ruby/container_spec.rb @@ -59,11 +59,9 @@ describe "#add_definition_path" do it "stores a new definition paths" do container = described_class.new - path = File.expand_path("./container/fixtures/definition_path.rb", __dir__) - binding.irb - container.add_definition_path(path) + container.add_definition_path(__FILE__) - expect(container.definition_paths).to include(path) + expect(container.definition_paths).to include(__FILE__) end end end From 06c5e23e54432008418d8cdad857f02511a48e00 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 01:02:08 +0100 Subject: [PATCH 05/10] Allow defining functions for custom containers --- lib/fun_ruby.rb | 5 +++-- spec/fun_ruby_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index df11e65..5684b28 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -22,8 +22,9 @@ 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 + Container::Define.build(container: target).(&block) end # Returns a global container diff --git a/spec/fun_ruby_spec.rb b/spec/fun_ruby_spec.rb index 2a65ed5..caafe08 100644 --- a/spec/fun_ruby_spec.rb +++ b/spec/fun_ruby_spec.rb @@ -24,4 +24,32 @@ 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 + end end From 12d6cc125eb781704a5b6ad4a30fcdd2b5ac51bb Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 01:14:36 +0100 Subject: [PATCH 06/10] Store the state of a definition path --- lib/fun_ruby/container.rb | 4 ++- lib/fun_ruby/container/definition_path.rb | 40 +++++++++++++++++++++++ spec/fun_ruby/container_spec.rb | 19 +++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 lib/fun_ruby/container/definition_path.rb diff --git a/lib/fun_ruby/container.rb b/lib/fun_ruby/container.rb index a7f9041..7aef75a 100644 --- a/lib/fun_ruby/container.rb +++ b/lib/fun_ruby/container.rb @@ -2,6 +2,7 @@ require_relative "container/mixin" require_relative "container/config" +require_relative "container/definition_path" module FunRuby # @private @@ -48,7 +49,8 @@ def import(*aliases) end # Adds a path to the definition path lit - def add_definition_path(path) + 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 diff --git a/lib/fun_ruby/container/definition_path.rb b/lib/fun_ruby/container/definition_path.rb new file mode 100644 index 0000000..9052560 --- /dev/null +++ b/lib/fun_ruby/container/definition_path.rb @@ -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 diff --git a/spec/fun_ruby/container_spec.rb b/spec/fun_ruby/container_spec.rb index c1a2253..db1252d 100644 --- a/spec/fun_ruby/container_spec.rb +++ b/spec/fun_ruby/container_spec.rb @@ -57,11 +57,26 @@ end describe "#add_definition_path" do - it "stores a new definition paths" do + it "stores a new definition paths as not loaded yet" do container = described_class.new container.add_definition_path(__FILE__) - expect(container.definition_paths).to include(__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 From 15db5eefb9d19388ec443471caf44754b2e83064 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 01:21:19 +0100 Subject: [PATCH 07/10] Saves a definition path as allready loaded where F.define is called --- lib/fun_ruby.rb | 2 ++ spec/fun_ruby_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index 5684b28..c4edb6a 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -24,6 +24,8 @@ module FunRuby # F.container.fetch("functions.sum").(2, 3) # => 5 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 diff --git a/spec/fun_ruby_spec.rb b/spec/fun_ruby_spec.rb index caafe08..b40d6f3 100644 --- a/spec/fun_ruby_spec.rb +++ b/spec/fun_ruby_spec.rb @@ -51,5 +51,23 @@ 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 end From 4770b92aa206eba7b90fc1f07574bcab35c88d75 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 01:37:35 +0100 Subject: [PATCH 08/10] Add API to add definition paths --- lib/fun_ruby.rb | 8 ++++++++ .../container_definitions/definition.rb | 0 .../container_definitions/nested/definition.rb | 0 spec/fun_ruby_spec.rb | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 spec/fixtures/container_definitions/definition.rb create mode 100644 spec/fixtures/container_definitions/nested/definition.rb diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index c4edb6a..d1d030b 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -29,6 +29,14 @@ def define(target = nil, &block) 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 + # Returns a global container # # @since 0.1.0 diff --git a/spec/fixtures/container_definitions/definition.rb b/spec/fixtures/container_definitions/definition.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/container_definitions/nested/definition.rb b/spec/fixtures/container_definitions/nested/definition.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/fun_ruby_spec.rb b/spec/fun_ruby_spec.rb index b40d6f3..906b1c5 100644 --- a/spec/fun_ruby_spec.rb +++ b/spec/fun_ruby_spec.rb @@ -70,4 +70,22 @@ 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 From 7a5991941a5a084416f23a46f8d2c31c6bf19711 Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 01:41:28 +0100 Subject: [PATCH 09/10] Add API to load registered container definitions --- lib/fun_ruby.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index d1d030b..546bde5 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -37,6 +37,14 @@ def add_definition_paths(glob, to: nil) 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 + # Returns a global container # # @since 0.1.0 From f1ac83c12b1c0fa91709d71ed46b29729aeb42bd Mon Sep 17 00:00:00 2001 From: woarewe Date: Fri, 17 Feb 2023 02:10:01 +0100 Subject: [PATCH 10/10] Add hot reloading --- Gemfile | 2 ++ Gemfile.lock | 8 ++++++++ lib/fun_ruby.rb | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/Gemfile b/Gemfile index 19f54a8..647b7a1 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,8 @@ source "https://rubygems.org" gemspec +gem "listen" + group :development do gem "activesupport" gem "dry-cli" diff --git a/Gemfile.lock b/Gemfile.lock index 5df19c7..65ca391 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) diff --git a/lib/fun_ruby.rb b/lib/fun_ruby.rb index 546bde5..7a6fdcc 100644 --- a/lib/fun_ruby.rb +++ b/lib/fun_ruby.rb @@ -45,6 +45,27 @@ def load_definitions!(target = nil) 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 # # @since 0.1.0