diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 198903e4..aff19735 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -6,7 +6,7 @@ jobs: strategy: fail-fast: false matrix: - ruby_version: ['2.6.3', '2.7.5', '3.0.5', '3.1.4', '3.2.2'] + ruby_version: ['2.7.5', '3.0.5', '3.1.4', '3.2.2'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.rspec b/.rspec index 34c5164d..8433ecd9 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ --format documentation --color --require spec_helper +--exclude-pattern "spec/rails_app/**/*" diff --git a/.rspec_rails_specs b/.rspec_rails_specs new file mode 100644 index 00000000..791d0cba --- /dev/null +++ b/.rspec_rails_specs @@ -0,0 +1,3 @@ +--format documentation +--color +--exclude-pattern "**/*" diff --git a/Gemfile b/Gemfile index 55e26d2d..df419cb2 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gemspec group :development, :test do gem "rails", '>= 6.1.4' gem "rspec-rails" + gem "parallel_tests" gem "pry", platform: :mri, require: false gem "pry-byebug", platform: :mri, require: false gem 'rubocop' diff --git a/lib/event_source.rb b/lib/event_source.rb index 0956d8c6..5135560e 100644 --- a/lib/event_source.rb +++ b/lib/event_source.rb @@ -41,6 +41,7 @@ require 'event_source/operations/fetch_session' require 'event_source/operations/build_message_options' require 'event_source/operations/build_message' +require 'event_source/boot_registry' # Event source provides ability to compose, publish and subscribe to events module EventSource @@ -64,14 +65,23 @@ class << self :async_api_schemas= def configure + @configured = true yield(config) end - def initialize! - load_protocols - create_connections - load_async_api_resources - load_components + def initialize!(force = false) + # Don't boot if I was never configured. + return unless @configured + boot_registry.boot!(force) do + load_protocols + create_connections + load_async_api_resources + load_components + end + end + + def boot_registry + @boot_registry ||= EventSource::BootRegistry.new end def config @@ -89,6 +99,14 @@ def build_async_api_resource(resource) .call(resource) .success end + + def register_subscriber(subscriber_klass) + boot_registry.register_subscriber(subscriber_klass) + end + + def register_publisher(subscriber_klass) + boot_registry.register_publisher(subscriber_klass) + end end class EventSourceLogger diff --git a/lib/event_source/boot_registry.rb b/lib/event_source/boot_registry.rb new file mode 100644 index 00000000..6c0a7b02 --- /dev/null +++ b/lib/event_source/boot_registry.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'set' +require 'monitor' + +module EventSource + # This class manages correct/loading of subscribers and publishers + # based on the current stage of the EventSource lifecycle. + # + # Depending on both the time the initialization of EventSource is invoked + # and when subscriber/publisher code is loaded, this can become complicated. + # This is largely caused by two confounding factors: + # 1. We want to delay initialization of EventSource until Rails is fully + # 'ready' + # 2. Based on the Rails environment, such as production, development, or + # test (primarily how those different environments treat lazy vs. eager + # loading of classes in a Rails application), subscriber and publisher + # code can be loaded before, after, or sometimes even DURING the + # EventSource boot process - we need to support all models + class BootRegistry + def initialize + @unbooted_publishers = Set.new + @unbooted_subscribers = Set.new + @booted_publishers = Set.new + @booted_subscribers = Set.new + # This is our re-entrant mutex. We're going to use it to make sure that + # registration and boot methods aren't allowed to simultaneously alter + # our state. You'll notice most methods on this class are wrapped in + # synchronize calls against this. + @bootex = Monitor.new + @booted = false + end + + def boot!(force = false) + @bootex.synchronize do + return if @booted && !force + yield + boot_publishers! + boot_subscribers! + @booted = true + end + end + + # Register a publisher for EventSource. + # + # If the EventSource hasn't been booted, save publisher for later. + # Otherwise, boot it now. + def register_publisher(publisher_klass) + @bootex.synchronize do + if @booted + publisher_klass.validate + @booted_publishers << publisher_klass + else + @unbooted_publishers << publisher_klass + end + end + end + + # Register a subscriber for EventSource. + # + # If the EventSource hasn't been booted, save the subscriber for later. + # Otherwise, boot it now. + def register_subscriber(subscriber_klass) + @bootex.synchronize do + if @booted + subscriber_klass.create_subscription + @booted_subscribers << subscriber_klass + else + @unbooted_subscribers << subscriber_klass + end + end + end + + # Boot the publishers. + def boot_publishers! + @bootex.synchronize do + @unbooted_publishers.each do |pk| + pk.validate + @booted_publishers << pk + end + @unbooted_publishers = Set.new + end + end + + # Boot the subscribers. + def boot_subscribers! + @bootex.synchronize do + @unbooted_subscribers.each do |sk| + sk.create_subscription + @booted_subscribers << sk + end + @unbooted_subscribers = Set.new + end + end + end +end \ No newline at end of file diff --git a/lib/event_source/protocols/amqp_protocol.rb b/lib/event_source/protocols/amqp_protocol.rb index c40594fe..5717a421 100644 --- a/lib/event_source/protocols/amqp_protocol.rb +++ b/lib/event_source/protocols/amqp_protocol.rb @@ -15,7 +15,7 @@ require_relative 'amqp/contracts/contract' Gem - .find_files('event_source/protocols/amqp/contracts/**/*.rb') + .find_files('event_source/protocols/amqp/contracts/**/*.rb', false) .sort .each { |f| require(f) } diff --git a/lib/event_source/protocols/http/faraday_queue_proxy.rb b/lib/event_source/protocols/http/faraday_queue_proxy.rb index c1b76c4e..e290909a 100644 --- a/lib/event_source/protocols/http/faraday_queue_proxy.rb +++ b/lib/event_source/protocols/http/faraday_queue_proxy.rb @@ -50,10 +50,12 @@ def actions # @return [Queue] Queue instance def subscribe(subscriber_klass, _options) unique_key = [app_name, formatted_exchange_name].join(delimiter) - logger.debug "FaradayQueueProxy#register_subscription Subscriber Class #{subscriber_klass}" - logger.debug "FaradayQueueProxy#register_subscription Unique_key #{unique_key}" - executable = subscriber_klass.executable_for(unique_key) - @subject.actions.push(executable) + subscription_key = [app_name, formatted_exchange_name].join(delimiter) + subscriber_suffix = subscriber_klass.name.downcase.gsub('::', '_') + unique_key = subscription_key + "_#{subscriber_suffix}" + logger.info "FaradayQueueProxy#register_subscription Subscriber Class #{subscriber_klass}" + logger.info "FaradayQueueProxy#register_subscription Unique_key #{unique_key}" + @subject.register_action(subscriber_klass, unique_key) end def consumer_proxy_for(operation_bindings) diff --git a/lib/event_source/publisher.rb b/lib/event_source/publisher.rb index d5aa9b3c..fc47e110 100644 --- a/lib/event_source/publisher.rb +++ b/lib/event_source/publisher.rb @@ -26,6 +26,16 @@ def self.publisher_container @publisher_container ||= Concurrent::Map.new end + def self.initialization_registry + @initialization_registry ||= Concurrent::Array.new + end + + def self.initialize_publishers + self.initialization_registry.each do |pub| + pub.validate + end + end + def self.[](exchange_ref) # TODO: validate publisher already exists # raise EventSource::Error::PublisherAlreadyRegisteredError.new(id) if registry.key?(id) @@ -46,12 +56,7 @@ def included(base) } base.extend(ClassMethods) - TracePoint.trace(:end) do |t| - if base == t.self - base.validate - t.disable - end - end + EventSource.register_publisher(base) end # methods to register events diff --git a/lib/event_source/queue.rb b/lib/event_source/queue.rb index 21dc7caa..bafeba03 100644 --- a/lib/event_source/queue.rb +++ b/lib/event_source/queue.rb @@ -6,15 +6,14 @@ class Queue # @attr_reader [Object] queue_proxy the protocol-specific class supporting this DSL # @attr_reader [String] name # @attr_reader [Hash] bindings - # @attr_reader [Hash] actions - attr_reader :queue_proxy, :name, :bindings, :actions + attr_reader :queue_proxy, :name, :bindings def initialize(queue_proxy, name, bindings = {}) @queue_proxy = queue_proxy @name = name @bindings = bindings @subject = ::Queue.new - @actions = [] + @registered_actions = [] end # def subscribe(subscriber_klass, &block) @@ -49,5 +48,16 @@ def close def closed? @subject.closed? end + + # Register an action to be performed, with a resolver class and key. + def register_action(resolver, key) + @registered_actions << [resolver, key] + end + + def actions + @registered_actions.map do |ra| + ra.first.executable_for(ra.last) + end + end end end diff --git a/lib/event_source/railtie.rb b/lib/event_source/railtie.rb index ea971274..77dcc66b 100644 --- a/lib/event_source/railtie.rb +++ b/lib/event_source/railtie.rb @@ -2,7 +2,9 @@ module EventSource # :nodoc: - class Railtie < Rails::Railtie - + module Railtie + Rails::Application::Finisher.initializer "event_source.boot", after: :finisher_hook do + EventSource.initialize! + end end -end \ No newline at end of file +end diff --git a/lib/event_source/subscriber.rb b/lib/event_source/subscriber.rb index d62257a6..937f2aba 100644 --- a/lib/event_source/subscriber.rb +++ b/lib/event_source/subscriber.rb @@ -49,12 +49,7 @@ def included(base) base.extend ClassMethods base.include InstanceMethods - TracePoint.trace(:end) do |t| - if base == t.self - base.create_subscription - t.disable - end - end + EventSource.register_subscriber(base) end module InstanceMethods diff --git a/spec/event_source/command_spec.rb b/spec/event_source/command_spec.rb index 7ad0f985..b44d0a6e 100644 --- a/spec/event_source/command_spec.rb +++ b/spec/event_source/command_spec.rb @@ -22,7 +22,12 @@ def call # binding.pry end + before :each do + EventSource.initialize!(true) + end + context '.event' do + let(:organization_params) do { hbx_id: '553234', diff --git a/spec/event_source/rails_application_spec.rb b/spec/event_source/rails_application_spec.rb new file mode 100644 index 00000000..7f77fa79 --- /dev/null +++ b/spec/event_source/rails_application_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" +require "parallel_tests" +require "parallel_tests/rspec/runner" + +RSpec.describe EventSource, "rails specs" do + it "runs the rails tests in the rails application context" do + ParallelTests.with_pid_file do + specs_run_result = ParallelTests::RSpec::Runner.run_tests( + [ + "spec/rails_app/spec/railtie_spec.rb", + "spec/rails_app/spec/http_service_integration_spec.rb" + ], + 1, + 1, + { + serialize_stdout: true, + test_options: ["-O", ".rspec_rails_specs", "--format", "documentation"] + } + ) + if specs_run_result[:exit_status] != 0 + fail(specs_run_result[:stdout] + "\n\n") + end + end + end +end \ No newline at end of file diff --git a/spec/rails_app/app/event_source/events/determinations/eval.rb b/spec/rails_app/app/event_source/events/determinations/eval.rb new file mode 100644 index 00000000..0cd6b90d --- /dev/null +++ b/spec/rails_app/app/event_source/events/determinations/eval.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Events + module Determinations + # Eval will register event publisher for MiTC + class Eval < EventSource::Event + publisher_path 'publishers.mitc_publisher' + + end + end +end \ No newline at end of file diff --git a/spec/rails_app/app/event_source/publishers/mitc_publisher.rb b/spec/rails_app/app/event_source/publishers/mitc_publisher.rb new file mode 100644 index 00000000..5fc3ed43 --- /dev/null +++ b/spec/rails_app/app/event_source/publishers/mitc_publisher.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Publishers + class MitcPublisher + # Publisher will send request payload to MiTC for determinations + include ::EventSource::Publisher[http: '/determinations/eval'] + register_event '/determinations/eval' + end +end diff --git a/spec/rails_app/app/event_source/publishers/parties/mitc_publisher.rb b/spec/rails_app/app/event_source/publishers/parties/mitc_publisher.rb deleted file mode 100644 index 3e007812..00000000 --- a/spec/rails_app/app/event_source/publishers/parties/mitc_publisher.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Parties - class MitcPublisher - # include ::EventSource::Publisher[http: '/determinations/eval'] - - # # register_event '/determinations/eval' - end -end - - - diff --git a/spec/rails_app/app/event_source/subscribers/mitc_response_subscriber.rb b/spec/rails_app/app/event_source/subscribers/mitc_response_subscriber.rb new file mode 100644 index 00000000..d9d253eb --- /dev/null +++ b/spec/rails_app/app/event_source/subscribers/mitc_response_subscriber.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Subscribers + class MitcResponseSubscriber + include ::EventSource::Subscriber[http: '/determinations/eval'] + extend EventSource::Logging + + subscribe(:on_determinations_eval) do |body, status, headers| + $GLOBAL_TEST_FLAG = true + end + end +end diff --git a/spec/rails_app/app/event_source/subscribers/parties/mitc_subscriber.rb b/spec/rails_app/app/event_source/subscribers/parties/mitc_subscriber.rb deleted file mode 100644 index a6a404f9..00000000 --- a/spec/rails_app/app/event_source/subscribers/parties/mitc_subscriber.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module MagiMedicaid - class EligbilityDeterminationsSubscriber - # include ::EventSource::Subscriber[http: '/determinations/eval'] - - # # # from: MagiMedicaidEngine of EA after Application's submission - # # # { event: magi_medicaid_application_submitted, payload: :magi_medicaid_application } - # subscribe(:on_determinations_eval) do |headers, payload| - # puts "block headers------#{headers}" - # puts "block payload-----#{payload}" - # end - end -end diff --git a/spec/rails_app/app/event_source/subscribers/parties/organization_subscriber.rb b/spec/rails_app/app/event_source/subscribers/parties/organization_subscriber.rb deleted file mode 100644 index b4d0786a..00000000 --- a/spec/rails_app/app/event_source/subscribers/parties/organization_subscriber.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Parties - class OrganizationSubscriber - # include ::EventSource::Subscriber[amqp: 'enroll.parties.organizations.fein_corrected'] - - # # subscribe(:on_enroll_parties_organizations_fein_corrected) do |delivery_info, metadata, payload| - # # # Sequence of steps that are executed as single operation - # # puts "triggered --> on_enroll_parties_organizations_fein_corrected block -- #{delivery_info} -- #{metadata} -- #{payload}" - # # end - - # subscribe("on_faa.enroll.parties.organizations") do |delivery_info, metadata, payload| - # # Sequence of steps that are executed as single operation - # puts "triggered --> on_enroll_parties_organizations_fein_corrected block -- #{delivery_info} -- #{metadata} -- #{payload}" - # end - # # def on_enroll_parties_organizations_fein_corrected(payload) - # # # Set of independent reactors for the given event that execute asynchronously - # # puts "triggered --> on_enroll_parties_organizations_fein_corrected method --#{payload}" - # # end - end -end \ No newline at end of file diff --git a/spec/rails_app/app/events/parties/organization/created.rb b/spec/rails_app/app/events/parties/organization/created.rb index 42866e23..49e6c4ff 100644 --- a/spec/rails_app/app/events/parties/organization/created.rb +++ b/spec/rails_app/app/events/parties/organization/created.rb @@ -6,7 +6,7 @@ class Created < EventSource::Event publisher_path 'parties.organization_publisher' # Schema used to validaate Event payload - contract_class 'Parties::Organization::CreateContract' + # contract_class 'Parties::Organization::CreateContract' attribute_keys :hbx_id, :legal_name, :fein, :entity_kind end end diff --git a/spec/rails_app/app/models/organizations/organization_model.rb b/spec/rails_app/app/models/organizations/organization_model.rb deleted file mode 100644 index 233218fc..00000000 --- a/spec/rails_app/app/models/organizations/organization_model.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Organizations - class OrganizationModel - include Mongoid::Document - include Mongoid::Timestamps - - field :legal_name, type: String - field :entity_kind, type: Symbol - field :fein, type: String - - # Track Events for this model - has_many :events, as: :event_stream, class_name: 'EventSource::EventStream' - end -end diff --git a/spec/rails_app/config/application.rb b/spec/rails_app/config/application.rb index bfe50a09..80441f98 100644 --- a/spec/rails_app/config/application.rb +++ b/spec/rails_app/config/application.rb @@ -7,8 +7,6 @@ Bundler.require(*Rails.groups) -require "event_source" - module RailsApp class Application < Rails::Application config.root = File.expand_path('../..', __FILE__) diff --git a/spec/rails_app/config/environments/test.rb b/spec/rails_app/config/environments/test.rb index 1ace3c3f..b2d790a8 100644 --- a/spec/rails_app/config/environments/test.rb +++ b/spec/rails_app/config/environments/test.rb @@ -12,7 +12,7 @@ # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + config.eager_load = true # Configure static file server for tests with Cache-Control for performance. config.serve_static_files = true diff --git a/spec/rails_app/config/initializers/event_source.rb b/spec/rails_app/config/initializers/event_source.rb index 346d942c..d192c56d 100644 --- a/spec/rails_app/config/initializers/event_source.rb +++ b/spec/rails_app/config/initializers/event_source.rb @@ -57,6 +57,14 @@ rabbitmq.default_content_type = 'application/json' end + server.http do |http| + http.ref = 'http://mitc:3001' + http.host = ENV['MITC_HOST'] || 'http://localhost' + http.port = ENV['MITC_PORT'] || '3000' + http.url = ENV['MITC_URL'] || 'http://localhost:3000' + http.default_content_type = 'application/json' + end + server.http do |http| http.host = "https://api.github.com" http.default_content_type = 'application/json' @@ -119,7 +127,8 @@ # AcaEntities::Operations::AsyncApi::FindResource.new.call(self) end -dir = Pathname.pwd.join('spec', 'support', 'async_api_files') +config_dir = File.dirname(__FILE__) +dir = File.join(config_dir, '..', '..', '..', 'support', 'async_api_files') EventSource.async_api_schemas = ::Dir[::File.join(dir, '**', '*')].reject { |p| ::File.directory? p }.sort.reduce([]) do |memo, file| # read # serialize yaml to hash @@ -127,4 +136,4 @@ memo << EventSource::AsyncApi::Operations::AsyncApiConf::LoadPath.new.call(path: file).success end -EventSource.initialize! +# EventSource.initialize! diff --git a/spec/rails_app/spec/http_service_integration_spec.rb b/spec/rails_app/spec/http_service_integration_spec.rb new file mode 100644 index 00000000..007eb983 --- /dev/null +++ b/spec/rails_app/spec/http_service_integration_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative './rails_helper' + +RSpec.describe "with an http subscriber service" do + include EventSource::Command + + it "runs when invoked" do + $GLOBAL_TEST_FLAG = false + + WebMock.stub_request( + :post, + /http:\/\/localhost:3000\/determinations\/eval/ + ).to_return({body: "{}"}) + response = event("events.determinations.eval", attributes: {}).success.publish + expect(response.status).to eq 200 + sleep(0.5) + expect($GLOBAL_TEST_FLAG).to eq(true) + end +end diff --git a/spec/rails_app/spec/rails_helper.rb b/spec/rails_app/spec/rails_helper.rb new file mode 100644 index 00000000..42d5ccf5 --- /dev/null +++ b/spec/rails_app/spec/rails_helper.rb @@ -0,0 +1,5 @@ +ENV['RAILS_ENV'] ||= 'test' +require 'bundler/setup' +require 'webmock/rspec' +require 'rails' +require_relative '../config/environment' diff --git a/spec/rails_app/spec/railtie_spec.rb b/spec/rails_app/spec/railtie_spec.rb new file mode 100644 index 00000000..7c93c25f --- /dev/null +++ b/spec/rails_app/spec/railtie_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative './rails_helper' + +RSpec.describe EventSource::Railtie do + it "runs when invoked" do + manager = EventSource::ConnectionManager.instance + connection = manager.connections_for(:amqp).first + expect(connection).not_to be_nil + end +end diff --git a/spec/support/async_api_files/http_mitc.yml b/spec/support/async_api_files/http_mitc.yml new file mode 100644 index 00000000..e7e2f4b6 --- /dev/null +++ b/spec/support/async_api_files/http_mitc.yml @@ -0,0 +1,58 @@ +asyncapi: "2.0.0" +info: + title: MAGI in the Cloud (MitC) + version: 0.1.0 + description: Configuration for accessing MitC Medicaid and CHIP eligibility determination services + contact: + name: IdeaCrew + url: https://ideacrew.com + email: info@ideacrew.com + license: + name: MIT + url: https://opensource.org/licenses/MIT + +servers: + production: + url: http://mitc:3001 + protocol: http + protocolVersion: 0.1.0 + description: MitC Development Server + development: + url: http://mitc:3001 + protocol: http + protocolVersion: 0.1.0 + description: MitC Development Server + test: + url: http://mitc:3001 + protocol: http + protocolVersion: 0.1.0 + description: MitC Test Server + +defaultContentType: application/json + +channels: + /determinations/eval: + publish: + operationId: /determinations/eval + description: HTTP endpoint for MitC eligibility determination requests + bindings: + http: + type: request + method: POST + headers: + Content-Type: application/json + Accept: application/json + subscribe: + operationId: /on/determinations/eval + description: EventSource Subscriber that publishes MitC eligibility determination responses + bindings: + http: + type: response + method: GET + headers: + Content-Type: application/json + Accept: application/json + +tags: + - name: linter_tag + description: placeholder that satisfies the linter \ No newline at end of file