diff --git a/examples/hello/hello.rb b/examples/hello/hello.rb index 17451ad..57f0d97 100755 --- a/examples/hello/hello.rb +++ b/examples/hello/hello.rb @@ -12,7 +12,7 @@ def setup(container) sleep 1 end end - end + end end service "sleep" do diff --git a/fixtures/async/service/good_interface.rb b/fixtures/async/service/good_interface.rb new file mode 100644 index 0000000..e5cb06f --- /dev/null +++ b/fixtures/async/service/good_interface.rb @@ -0,0 +1,9 @@ +module Async + module Service + module GoodInterface + def good_method + :good + end + end + end +end diff --git a/fixtures/async/service/sleep_service.rb b/fixtures/async/service/sleep_service.rb new file mode 100644 index 0000000..c4e8203 --- /dev/null +++ b/fixtures/async/service/sleep_service.rb @@ -0,0 +1,36 @@ +require 'async/service/generic' + +module Async + module Service + class SleepService < Async::Service::Generic + def setup(container) + super + + container.run(count: 1, restart: true) do |instance| + # Use log level: + Console.logger.level = @environment.evaluator.log_level + + if container.statistics.failed? + Console.debug(self, "Child process restarted #{container.statistics.restarts} times.") + else + Console.debug(self, "Child process started.") + end + + instance.ready! + + while true + sleep 1 + + Console.debug(self, "Work") + + if rand < 0.1 + Console.debug(self, "Should exit...") + sleep 0.5 + exit(1) + end + end + end + end + end + end +end diff --git a/lib/async/service/configuration.rb b/lib/async/service/configuration.rb index b7f275f..333451a 100644 --- a/lib/async/service/configuration.rb +++ b/lib/async/service/configuration.rb @@ -51,9 +51,11 @@ def services(implementing: nil) return to_enum(:services, implementing: implementing) unless block_given? @environments.each do |environment| - next if implementing and environment.implements?(implementing) - - yield Generic.wrap(environment) + if implementing.nil? or environment.implements?(implementing) + if service = Generic.wrap(environment) + yield service + end + end end end diff --git a/lib/async/service/environment.rb b/lib/async/service/environment.rb index 898401f..1fd4acd 100644 --- a/lib/async/service/environment.rb +++ b/lib/async/service/environment.rb @@ -67,6 +67,10 @@ def method_missing(name, argument = nil, &block) @facet.define_method(name){argument} end end + + def respond_to_missing?(name, include_private = false) + true + end end def self.build(...) diff --git a/lib/async/service/generic.rb b/lib/async/service/generic.rb index 8f6fdfc..b2b306c 100644 --- a/lib/async/service/generic.rb +++ b/lib/async/service/generic.rb @@ -12,11 +12,14 @@ module Service class Generic # Convert the given environment into a service if possible. # @parameter environment [Build::Environment] The environment to use to construct the service. + # @returns [Generic | Nil] The constructed service if the environment specifies a service class. def self.wrap(environment) evaluator = environment.evaluator - if service_class = evaluator.service_class || self - return service_class.new(environment, evaluator) + if evaluator.key?(:service_class) + if service_class = evaluator.service_class + return service_class.new(environment, evaluator) + end end end diff --git a/lib/async/service/loader.rb b/lib/async/service/loader.rb index f58d355..af3e48f 100644 --- a/lib/async/service/loader.rb +++ b/lib/async/service/loader.rb @@ -47,11 +47,14 @@ def environment(**initial, &block) Environment.build(**initial, &block) end - # Define a host with the specified name. - # Adds `root` and `authority` keys. + # Define a service with the specified name. + # Adds `root` and `name` keys. # @parameter name [String] The name of the environment, usually a hostname. - def service(name, &block) - @configuration.add(self.environment(name: name, root: @root, &block)) + def service(name = nil, **options, &block) + options[:name] = name + options[:root] ||= @root + + @configuration.add(self.environment(**options, &block)) end end end diff --git a/test/async/service/.configurations/implementing.rb b/test/async/service/.configurations/implementing.rb new file mode 100644 index 0000000..fc0d4b9 --- /dev/null +++ b/test/async/service/.configurations/implementing.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env async-service +# frozen_string_literal: trueb + +# Released under the MIT License. +# Copyright, 2025, by Samuel Williams. + +require "async/service/good_interface" +require "async/service/sleep_service" + +# A service without a service class, e.g. a no-op. +service "good-service" do + include Async::Service::GoodInterface + + service_class Async::Service::SleepService +end + +service "not-so-good-service" do + service_class Async::Service::SleepService +end diff --git a/test/async/service/.configurations/null.rb b/test/async/service/.configurations/null.rb new file mode 100644 index 0000000..c1ff1c8 --- /dev/null +++ b/test/async/service/.configurations/null.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env async-service +# frozen_string_literal: trueb + +# Released under the MIT License. +# Copyright, 2025, by Samuel Williams. + +# A service without a service class, e.g. a no-op. +service "null" do +end diff --git a/test/async/service/.configurations/sleep.rb b/test/async/service/.configurations/sleep.rb index b83ce90..1e05044 100755 --- a/test/async/service/.configurations/sleep.rb +++ b/test/async/service/.configurations/sleep.rb @@ -8,37 +8,7 @@ log_level :info end -# A test service: -class SleepService < Async::Service::Generic - def setup(container) - super - - container.run(count: 1, restart: true) do |instance| - # Use log level: - Console.logger.level = @environment.evaluator.log_level - - if container.statistics.failed? - Console.debug(self, "Child process restarted #{container.statistics.restarts} times.") - else - Console.debug(self, "Child process started.") - end - - instance.ready! - - while true - sleep 1 - - Console.debug(self, "Work") - - if rand < 0.1 - Console.debug(self, "Should exit...") - sleep 0.5 - exit(1) - end - end - end - end -end +require "async/service/sleep_service" service "sleep" do include LogLevel @@ -46,7 +16,7 @@ def setup(container) authority {self.name} middleware {Object.new} - service_class SleepService + service_class Async::Service::SleepService end # A 2nd service: diff --git a/test/async/service/configuration.rb b/test/async/service/configuration.rb index f2b0060..80a67cd 100644 --- a/test/async/service/configuration.rb +++ b/test/async/service/configuration.rb @@ -4,6 +4,7 @@ # Copyright, 2024, by Samuel Williams. require 'async/service/configuration' +require "async/service/good_interface" describe Async::Service::Configuration do with '.build' do @@ -29,6 +30,42 @@ end end + with "null serice configuration file" do + let(:configuration_path) {File.join(__dir__, '.configurations', 'null.rb')} + + let(:configuration) do + subject.new.tap do |configuration| + configuration.load_file(configuration_path) + end + end + + it 'can load configuration' do + expect(configuration).not.to be(:empty?) + + environment = configuration.environments.first + evaluator = environment.evaluator + expect(evaluator.name).to be == 'null' + + expect(configuration.services.to_a).to be(:empty?) + end + end + + with "implementing service configuration file" do + let(:configuration_path) {File.join(__dir__, '.configurations', 'implementing.rb')} + + let(:configuration) do + subject.new.tap do |configuration| + configuration.load_file(configuration_path) + end + end + + it "can select services by implementing module" do + expect(configuration).not.to be(:empty?) + + expect(configuration.services(implementing: Async::Service::GoodInterface).to_a).not.to be(:empty?) + end + end + with 'sleep service configuration file' do let(:configuration_path) {File.join(__dir__, '.configurations', 'sleep.rb')} let(:configuration_root) {File.join(File.realpath(__dir__), '.configurations')}