From c24e4efd10eac8110119bfa49f3bf65d30023821 Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Thu, 21 Dec 2023 17:44:53 -0500 Subject: [PATCH 1/4] Update Message and dependent contracts and specs --- .../contracts/message_binding_contract.rb | 23 ++++ .../async_api/contracts/message_contract.rb | 49 ++++--- .../async_api/contracts/schema_contract.rb | 23 ++++ .../contracts/schema_object_contract.rb | 28 ++++ .../message_binding_contract_spec.rb | 33 +++++ .../contracts/message_contract_spec.rb | 128 +++++++++--------- .../contracts/schema_contract_spec.rb | 37 +++++ .../contracts/schema_object_contract_spec.rb | 66 +++++++++ 8 files changed, 305 insertions(+), 82 deletions(-) create mode 100644 lib/event_source/async_api/contracts/message_binding_contract.rb create mode 100644 lib/event_source/async_api/contracts/schema_contract.rb create mode 100644 lib/event_source/async_api/contracts/schema_object_contract.rb create mode 100644 spec/event_source/async_api/contracts/message_binding_contract_spec.rb create mode 100644 spec/event_source/async_api/contracts/schema_contract_spec.rb create mode 100644 spec/event_source/async_api/contracts/schema_object_contract_spec.rb diff --git a/lib/event_source/async_api/contracts/message_binding_contract.rb b/lib/event_source/async_api/contracts/message_binding_contract.rb new file mode 100644 index 00000000..1e1f75ad --- /dev/null +++ b/lib/event_source/async_api/contracts/message_binding_contract.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module EventSource + module AsyncApi + module Contracts + # Schema and validation rules for {EventSource::AsyncApi::MessageBinding} domain object + class MessageBindingContract < Contract + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [String] :content_encoding optional + # @option opts [String] :message_type optional + # @option opts [String] :binding_version optional + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + optional(:content_encoding).maybe(:string) + optional(:message_type).maybe(:string) + optional(:binding_version).maybe(:string) + end + end + end + end +end diff --git a/lib/event_source/async_api/contracts/message_contract.rb b/lib/event_source/async_api/contracts/message_contract.rb index e09e9a63..11469e72 100644 --- a/lib/event_source/async_api/contracts/message_contract.rb +++ b/lib/event_source/async_api/contracts/message_contract.rb @@ -1,48 +1,65 @@ # frozen_string_literal: true +require 'event_source/async_api/contracts/schema_contract' +require 'event_source/async_api/contracts/message_binding_contract' + module EventSource module AsyncApi module Contracts # Schema and validation rules for {EventSource::AsyncApi::Message} - class MessageContract < Contract + class MessageContract < Dry::Validation::Contract # @!method call(opts) # @param [Hash] opts the parameters to validate using this contract - # @option opts [Hash] :headers optional - # @option opts [Mixed] :payload optional - # @option opts [String] :schema_format optional + # @option opts [EventSource::AsyncApi::Contracts::SchemaContract] :headers optional + # @option opts [EventSource::AsyncApi::Contracts::SchemaContract] :payload optional # @option opts [String] :content_type optional # @option opts [String] :name optional # @option opts [String] :title optional # @option opts [String] :summary optional # @option opts [String] :description optional - # @option opts [Array] :tags optional + # @option opts [Array] :tags optional # @option opts [ExternalDocumentation] :external_docs optional - # @option opts [Hash] :bindings optional + # @option opts [EventSource::AsyncApi::Contracts::MessageBindingContract] :bindings optional # @option opts [Array] :examples optional # @option opts [Array] :traits optional - # @return [Dry::Monads::Result::Success, Dry::Monads::Result::Failure] + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation params do - optional(:headers).maybe(Types::HashOrNil) - optional(:payload).maybe(:any) - optional(:schema_format).maybe(:string) - optional(:contentType).maybe(:string) + optional(:headers).maybe(EventSource::AsyncApi::Contracts::SchemaContract.params) + optional(:payload).maybe(EventSource::AsyncApi::Contracts::SchemaContract.params) + optional(:correlation_id).hash do + optional(:description).maybe(:string) + required(:location).filled(:string) + end + optional(:content_type).maybe(:string) optional(:name).maybe(:string) optional(:title).maybe(:string) optional(:summary).maybe(:string) optional(:description).maybe(:string) - optional(:tags).array(Types::HashOrNil) + optional(:tags).array(EventSource::AsyncApi::Contracts::TagContract.params) optional(:external_docs).array(Types::HashOrNil) - optional(:bindings).maybe(Types::HashOrNil) + optional(:bindings).maybe(EventSource::AsyncApi::Contracts::MessageBindingContract.params) optional(:examples).maybe(Types::HashOrNil) optional(:traits).array(Types::HashOrNil) + # Coerce empty attributes with default values before(:value_coercer) do |result| - if result.to_h.key?(:external_docs) && - result.to_h[:external_docs].nil? + if result.to_h.key?(:external_docs).nil? || result.to_h[:external_docs].nil? + result.to_h.merge!(default_external_docs) + end - result.to_h.merge!({ external_docs: Array.new }) + if (result.to_h.key?(:correlation_id).nil? || result.to_h[:correlation_id].nil?) + result.to_h.merge!(default_correlation_id) end end + + def default_external_docs + { external_docs: Array.new } + end + + def default_correlation_id + { description: 'Default Correlation ID', location: '$message.header#/correlationId' } + end end end end diff --git a/lib/event_source/async_api/contracts/schema_contract.rb b/lib/event_source/async_api/contracts/schema_contract.rb new file mode 100644 index 00000000..ac160364 --- /dev/null +++ b/lib/event_source/async_api/contracts/schema_contract.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'event_source/async_api/contracts/schema_object_contract' + +module EventSource + module AsyncApi + module Contracts + # Schema and validation rules for {EventSource::AsyncApi::Schema} domain object + class SchemaContract < Contract + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [String] :schema_format optional + # @option opts [EventSource::AcaEntities::SchemaObjectContract] :schema optional + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + optional(:schema_format).maybe(:string) + optional(:schema).maybe(EventSource::AsyncApi::Contracts::SchemaObjectContract.params) + end + end + end + end +end diff --git a/lib/event_source/async_api/contracts/schema_object_contract.rb b/lib/event_source/async_api/contracts/schema_object_contract.rb new file mode 100644 index 00000000..d9519531 --- /dev/null +++ b/lib/event_source/async_api/contracts/schema_object_contract.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module EventSource + module AsyncApi + module Contracts + # Schema and validation rules for {EventSource::AsyncApi::SchemaObject} domain object + class SchemaObjectContract < Contract + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [String] :type required + # @option opts [String] :required optional + # @option opts [Hash] :properties + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + required(:type).filled(:string) + optional(:required).array(:string) + optional(:properties).maybe(:hash) + end + + rule(:required).each do + property_keys = result.to_h[:properties].keys + key.failure("#{value}: not defined in properties") unless property_keys.include? value.to_sym + end + end + end + end +end diff --git a/spec/event_source/async_api/contracts/message_binding_contract_spec.rb b/spec/event_source/async_api/contracts/message_binding_contract_spec.rb new file mode 100644 index 00000000..733d6095 --- /dev/null +++ b/spec/event_source/async_api/contracts/message_binding_contract_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Contracts::MessageBindingContract do + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:optional_params) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } + end + + describe '#call' do + subject(:message_binding) { described_class.new } + + context 'Given empty parameters' do + it 'returns monad success' do + expect(message_binding.call({}).success?).to be_truthy + end + end + + context 'Given optional only parameters' do + it 'returns monad success' do + expect(message_binding.call(optional_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(message_binding.call(optional_params).to_h).to eq optional_params + end + end + end +end diff --git a/spec/event_source/async_api/contracts/message_contract_spec.rb b/spec/event_source/async_api/contracts/message_contract_spec.rb index 09fbf058..611ead94 100644 --- a/spec/event_source/async_api/contracts/message_contract_spec.rb +++ b/spec/event_source/async_api/contracts/message_contract_spec.rb @@ -3,104 +3,100 @@ require 'spec_helper' RSpec.describe EventSource::AsyncApi::Contracts::MessageContract do - let(:name) { 'UserSignup' } - let(:title) { 'User signup' } - let(:summary) { 'Action to sign a user up.' } - let(:description) { 'A longer description' } - let(:contentType) { 'application/json' } - let(:tags) do - [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] - end + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } - let(:header_type) { 'object' } - let(:header_correlation_id) do - { description: 'Correlation ID set by application', type: 'string' } + let(:header_schema_object_properties) do + { correlation_id: correlation_id_property, occurred_at: occurred_at_property } end - let(:application_instance_id) do + let(:header_schema_object_type) { 'object' } + let(:header_schema_object_required) { %w[correlation_id] } + + let(:header_schema_object) do { - description: - 'Unique identifier for a given instance of the publishing application', - type: 'string' + type: header_schema_object_type, + required: header_schema_object_required, + properties: header_schema_object_properties } end - let(:header_properties) do + + let(:header_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:header_schema) { { schema_format: header_schema_format, schema: header_schema_object } } + + let(:provider_property) { { type: 'string', description: 'Third party OAuth service that authenticates account' } } + let(:uid_property) { { type: 'string', description: 'Provider-assigned unique account identifier' } } + + let(:payload_schema_object_properties) { { provider: provider_property, uid: uid_property } } + let(:payload_schema_object_type) { 'object' } + let(:payload_schema_object_required) { %w[provider uid] } + + let(:payload_schema_object) do { - correlation_id: header_correlation_id, - application_instance_id: application_instance_id + type: payload_schema_object_type, + required: payload_schema_object_required, + properties: payload_schema_object_properties } end - let(:headers) { { type: header_type, properties: header_properties } } - let(:payload_type) { 'object' } - let(:user) { { "$ref": '#/components/schemas/userCreate' } } - let(:signup) { { "$ref": '#/components/schemas/signup' } } - let(:payload_properties) { { user: user, signup: signup } } + let(:payload_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:payload_schema) { { schema_format: payload_schema_format, schema: payload_schema_object } } - let(:correlation_id) do - { description: 'Correlation ID set by application', type: 'string' } - end + let(:content_type) { 'application/json' } + let(:name) { 'UserSignup' } + let(:title) { 'User signup' } + let(:summary) { 'Action to sign a user up.' } + let(:description) { 'A longer description' } + let(:tags) { [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] } - let(:payload) { { type: payload_type, properties: payload_properties } } - let(:correlation_id) do - { - description: 'Default Correlation ID', - location: '$message.header#/correlation_id' - } + let(:correlation_id) { { description: 'Default Correlation ID', location: '$message.header#/correlation_id' } } + let(:traits) { [{ '$ref': '#/components/messageTraits/commonHeaders' }] } + + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:message_binding) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } end - let(:traits) { [{ "$ref": '#/components/messageTraits/commonHeaders' }] } - let(:schema_format) { nil } - let(:content_type) { nil } let(:external_docs) { [] } let(:bindings) { nil } let(:examples) { nil } let(:optional_params) do { - headers: headers, - payload: payload, + headers: header_schema, + payload: payload_schema, + correlation_id: correlation_id, + content_type: content_type, name: name, title: title, summary: summary, description: description, - traits: traits, tags: tags, - schema_format: schema_format, - contentType: content_type, + bindings: message_binding, external_docs: external_docs, - bindings: bindings, - examples: examples + examples: examples, + traits: traits } end describe '#call' do + subject(:message) { described_class.new } + context 'Given empty parameters' do - it { expect(subject.call({}).success?).to be_truthy } + it 'returns monad success' do + expect(message.call({}).success?).to be_truthy + end end - context 'Given valid parameters' do - context 'and optional parameters' do - it 'should successfully return all optional params as attributes' do - result = subject.call(optional_params) - - expect(result.success?).to be_truthy - expect(result.to_h).to eq optional_params - - expect(result[:headers]).to eq headers - expect(result[:payload]).to eq payload - expect(result[:name]).to eq name - expect(result[:title]).to eq title - expect(result[:summary]).to eq summary - expect(result[:description]).to eq description - expect(result[:tags]).to eq tags - expect(result[:traits]).to eq traits - - expect(result[:schema_format]).to eq schema_format - expect(result[:contentType]).to eq content_type - expect(result[:external_docs]).to eq external_docs - expect(result[:bindings]).to eq bindings - expect(result[:examples]).to eq examples - end + context 'Given optional only parameters' do + it 'returns monad success' do + expect(message.call(optional_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(message.call(optional_params).to_h).to eq optional_params end end end diff --git a/spec/event_source/async_api/contracts/schema_contract_spec.rb b/spec/event_source/async_api/contracts/schema_contract_spec.rb new file mode 100644 index 00000000..632abd7f --- /dev/null +++ b/spec/event_source/async_api/contracts/schema_contract_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Contracts::SchemaContract do + let(:type) { 'object' } + let(:required) { %w[correlation_id] } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + let(:properties) { { correlation_id: correlation_id_property, occurred_at: occurred_at_property } } + let(:schema) { { type: type, required: required, properties: properties } } + + let(:schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + + let(:optional_params) { { schema_format: schema_format, schema: schema } } + + describe '#call' do + subject(:schema_instance) { described_class.new } + + context 'Given empty parameters' do + it 'returns monad success' do + expect(schema_instance.call({}).success?).to be_truthy + end + end + + context 'Given optional only parameters' do + it 'returns monad success' do + expect(schema_instance.call(optional_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(schema_instance.call(optional_params).to_h).to eq optional_params + end + end + end +end diff --git a/spec/event_source/async_api/contracts/schema_object_contract_spec.rb b/spec/event_source/async_api/contracts/schema_object_contract_spec.rb new file mode 100644 index 00000000..fe750b51 --- /dev/null +++ b/spec/event_source/async_api/contracts/schema_object_contract_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Contracts::SchemaObjectContract do + let(:type) { 'object' } + let(:required) { %w[correlation_id] } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + let(:properties) { { correlation_id: correlation_id_property, occurred_at: occurred_at_property } } + + let(:required_params) { { type: type } } + let(:optional_params) { { required: required, properties: properties } } + let(:all_params) { required_params.merge(optional_params) } + + describe '#call' do + subject(:schema_object) { described_class.new } + + context 'Given empty parameters' do + it 'returns monad failure' do + expect(schema_object.call({}).failure?).to be_truthy + end + end + + context 'Given optional only parameters' do + it 'returns monad failure' do + expect(schema_object.call(optional_params).failure?).to be_truthy + end + end + + context 'Given required parameters only' do + it 'returns monad success' do + expect(schema_object.call(required_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(schema_object.call(required_params).to_h).to eq required_params + end + end + + context 'Given all required and optional parameters' do + it 'returns monad success' do + expect(schema_object.call(all_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(schema_object.call(all_params).to_h).to eq all_params + end + end + + context 'Given a required property key thats not defined in properties hash' do + let(:undefined_property) { 'undefined_property' } + let(:invalid_params) { all_params.merge({ required: [undefined_property] }) } + let(:error) { { required: { 0 => ['undefined_property: not defined in properties'] } } } + + it 'returns monad failure' do + expect(schema_object.call(invalid_params).failure?).to be_truthy + end + + it 'returns an error for the undefined property' do + expect(schema_object.call(invalid_params).errors.to_h).to eq error + end + end + end +end From c52a66a076a9fee2532b54ad645b29423abf7b2b Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Fri, 22 Dec 2023 16:19:01 -0500 Subject: [PATCH 2/4] Update Dry gems. Moved dev gems to Gemfil --- .rubocop.yml | 3 + Gemfile | 16 +- Gemfile.lock | 516 ++++++++++++++++++++++++------------------- event_source.gemspec | 29 +-- lib/event_source.rb | 9 +- 5 files changed, 314 insertions(+), 259 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e35fac10..dc58e281 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,7 @@ # This yaml describes our current checks. +require: + - rubocop-performance + AllCops: TargetRubyVersion: 2.5 SuggestExtensions: false diff --git a/Gemfile b/Gemfile index 55e26d2d..4a2eceb6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,21 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in event_source.gemspec gemspec group :development, :test do - gem "rails", '>= 6.1.4' - gem "rspec-rails" - gem "pry", platform: :mri, require: false - gem "pry-byebug", platform: :mri, require: false + gem 'database_cleaner' + gem 'faker' + gem 'mongoid' + gem 'pry', platform: :mri, require: false + gem 'pry-byebug', platform: :mri, require: false + gem 'rails', '>= 6.1.4' + gem 'rspec-rails' gem 'rubocop' + gem 'rubocop-performance', require: false + gem 'sinatra' + gem 'webmock' gem 'yard' end diff --git a/Gemfile.lock b/Gemfile.lock index e2546959..e13c2aaf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,15 +5,11 @@ PATH addressable (>= 2.8.0) bunny (>= 2.14) deep_merge (~> 1.2.0) - dry-configurable (~> 0.12) - dry-events (~> 0.3) - dry-inflector (~> 0.2) - dry-initializer (~> 3.0) - dry-monads (~> 1.3) - dry-schema (~> 1.6) - dry-struct (~> 1.4) - dry-types (~> 1.5) - dry-validation (~> 1.6) + dry-configurable (~> 1.0) + dry-events (~> 1.0) + dry-monads (~> 1.6) + dry-struct (~> 1.6) + dry-validation (~> 1.10) faraday (~> 1.4.1) faraday_middleware (~> 1.0) logging (~> 2.3.0) @@ -27,139 +23,160 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) - actionmailer (6.1.4.1) - actionpack (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activesupport (= 6.1.4.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.4.1) - actionview (= 6.1.4.1) - activesupport (= 6.1.4.1) - rack (~> 2.0, >= 2.0.9) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.1) - actionpack (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4.1) - activesupport (= 6.1.4.1) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4.1) - activesupport (= 6.1.4.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (6.1.4.1) - activesupport (= 6.1.4.1) - activerecord (6.1.4.1) - activemodel (= 6.1.4.1) - activesupport (= 6.1.4.1) - activestorage (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activesupport (= 6.1.4.1) - marcel (~> 1.0.0) - mini_mime (>= 1.1.0) - activesupport (6.1.4.1) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) + timeout (>= 0.4.0) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) + marcel (~> 1.0) + activesupport (7.1.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) amq-protocol (2.3.2) ast (2.4.2) - bson (4.12.1) + base64 (0.2.0) + bigdecimal (3.1.5) + bson (4.15.0) builder (3.2.4) - bunny (2.19.0) + bunny (2.22.0) amq-protocol (~> 2.3, >= 2.3.1) sorted_set (~> 1, >= 1.0.2) byebug (11.1.3) coderay (1.1.3) - concurrent-ruby (1.1.9) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) - database_cleaner (2.0.1) - database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.1) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - deep_merge (1.2.1) - diff-lcs (1.4.4) - dry-configurable (0.12.1) - concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5.0) - dry-container (0.8.0) - concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.7.1) + date (3.3.4) + deep_merge (1.2.2) + diff-lcs (1.5.0) + drb (2.2.0) + ruby2_keywords + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.1) concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-events (0.3.0) + zeitwerk (~> 2.6) + dry-events (1.0.1) concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5) - dry-inflector (0.2.1) - dry-initializer (3.0.4) - dry-logic (1.2.0) + dry-core (~> 1.0, < 2) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5) - dry-monads (1.4.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-monads (1.6.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.7) - dry-schema (1.6.2) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-schema (1.13.3) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.8, >= 0.8.3) - dry-core (~> 0.5, >= 0.5) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-logic (~> 1.0) - dry-types (~> 1.5) - dry-struct (1.4.0) - dry-core (~> 0.5, >= 0.5) - dry-types (~> 1.5) + dry-logic (>= 1.4, < 2) + dry-types (>= 1.7, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) ice_nine (~> 0.11) - dry-types (1.5.1) + zeitwerk (~> 2.6) + dry-types (1.7.1) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.5, >= 0.5) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.0, >= 1.0.2) - dry-validation (1.6.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + dry-validation (1.10.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-schema (~> 1.5, >= 1.5.2) - erubi (1.10.0) - et-orbi (1.2.6) + dry-schema (>= 1.12, < 2) + zeitwerk (~> 2.6) + erubi (1.12.0) + et-orbi (1.2.7) tzinfo - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) - faker (2.18.0) - i18n (>= 1.6, < 2) + faker (3.2.2) + i18n (>= 1.8.11, < 2) faraday (1.4.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -175,99 +192,142 @@ GEM faraday-net_http_persistent (1.2.0) faraday_middleware (1.2.0) faraday (~> 1.0) - ffi (1.15.4) - fugit (1.5.2) - et-orbi (~> 1.1, >= 1.1.8) + ffi (1.16.3) + fugit (1.9.0) + et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) - globalid (0.5.2) - activesupport (>= 5.0) - hashdiff (1.0.1) - i18n (1.8.10) + globalid (1.2.1) + activesupport (>= 6.1) + hashdiff (1.1.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) ice_nine (0.11.2) + io-console (0.7.1) + irb (1.11.0) + rdoc + reline (>= 0.3.8) + json (2.7.1) + language_server-protocol (3.17.0.3) little-plugger (1.1.4) - logging (2.3.0) + logging (2.3.1) little-plugger (~> 1.1) multi_json (~> 1.14) - loofah (2.12.0) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marcel (1.0.2) method_source (1.0.0) - mime-types (3.3.1) + mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) - mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.14.4) - mongo (2.14.0) - bson (>= 4.8.2, < 5.0.0) - mongoid (7.3.0) - activemodel (>= 5.1, < 6.2) - mongo (>= 2.10.5, < 3.0.0) - mono_logger (1.1.1) + mime-types-data (3.2023.1205) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + minitest (5.20.0) + mongo (2.19.3) + bson (>= 4.14.1, < 5.0.0) + mongoid (8.1.4) + activemodel (>= 5.1, < 7.2, != 7.0.0) + concurrent-ruby (>= 1.0.5, < 2.0) + mongo (>= 2.18.0, < 3.0.0) + ruby2_keywords (~> 0.0.5) + mono_logger (1.1.2) multi_json (1.15.0) - multipart-post (2.1.1) - mustermann (1.1.1) + multipart-post (2.3.0) + mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - nio4r (2.5.8) - oj (3.13.9) - ox (2.14.5) - parallel (1.20.1) - parser (3.0.1.1) + mutex_m (0.2.0) + net-imap (0.4.8) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.4.0) + net-protocol + nio4r (2.7.0) + nokogiri (1.15.5) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + oj (3.16.3) + bigdecimal (>= 3.0) + ox (2.14.17) + parallel (1.24.0) + parser (3.2.2.4) ast (~> 2.4.1) - pry (0.14.1) + racc + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.8.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.10) - public_suffix (4.0.6) - queue-bus (0.12.0) + pry (>= 0.13, < 0.15) + psych (5.1.2) + stringio + public_suffix (5.0.4) + queue-bus (0.13.2) multi_json redis raabro (1.4.0) - racc (1.6.2) - rack (2.2.3) - rack-protection (2.1.0) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.4.1) - actioncable (= 6.1.4.1) - actionmailbox (= 6.1.4.1) - actionmailer (= 6.1.4.1) - actionpack (= 6.1.4.1) - actiontext (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activemodel (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + racc (1.7.3) + rack (2.2.8) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) + rack-session (1.0.2) + rack (< 3) + rack-test (2.1.0) + rack (>= 1.3) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.2) + actioncable (= 7.1.2) + actionmailbox (= 7.1.2) + actionmailer (= 7.1.2) + actionpack (= 7.1.2) + actiontext (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activemodel (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) bundler (>= 1.15.0) - railties (= 6.1.4.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.1.2) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) - method_source - rake (>= 0.13) - thor (~> 1.0) - rainbow (3.0.0) - rake (13.0.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.1.0) rbtree (0.4.6) - redis (4.5.1) - redis-namespace (1.8.1) - redis (>= 3.0.4) - regexp_parser (2.1.1) + rdoc (6.6.2) + psych (>= 4.0.0) + redis (5.0.8) + redis-client (>= 0.17.0) + redis-client (0.19.1) + connection_pool + redis-namespace (1.11.0) + redis (>= 4) + regexp_parser (2.8.3) + reline (0.4.1) + io-console (~> 0.5) resque (1.27.4) mono_logger (~> 1.0) multi_json (~> 1.0) @@ -279,81 +339,82 @@ GEM resque (>= 1.10.0, < 2.0) resque-retry resque-scheduler (>= 2.0.1) - resque-retry (1.7.6) + resque-retry (1.8.1) resque (>= 1.25, < 3.0) - resque-scheduler (~> 4.0) - resque-scheduler (4.5.0) + resque-scheduler (>= 4.0, < 6.0) + resque-scheduler (4.10.2) mono_logger (~> 1.0) redis (>= 3.3) resque (>= 1.27) - rufus-scheduler (~> 3.2, < 3.7) - rexml (3.2.5) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rufus-scheduler (~> 3.2, != 3.3) + rexml (3.2.6) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.1) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.2) - rubocop (1.10.0) + rspec-support (~> 3.12.0) + rspec-rails (6.1.0) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) + rubocop (1.59.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.2.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.7.0) - parser (>= 3.0.1.1) - ruby-progressbar (1.11.0) - ruby2_keywords (0.0.4) - rufus-scheduler (3.6.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-performance (1.20.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) - set (1.0.2) - sinatra (2.1.0) - mustermann (~> 1.0) - rack (~> 2.2) - rack-protection (= 2.1.0) + set (1.0.4) + sinatra (3.1.0) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.1.0) tilt (~> 2.0) sorted_set (1.0.3) rbtree set (~> 1.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.1.0) - tilt (2.0.10) - typhoeus (1.4.0) + stringio (3.1.0) + thor (1.3.0) + tilt (2.3.0) + timeout (0.4.1) + typhoeus (1.4.1) ethon (>= 0.9.0) - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.0.0) + unicode-display_width (2.5.0) vegas (0.1.11) rack (>= 1.0.0) - webmock (3.13.0) - addressable (>= 2.3.6) + webmock (3.19.1) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.5) + webrick (1.8.1) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - yard (0.9.26) - zeitwerk (2.5.1) + yard (0.9.34) + zeitwerk (2.6.12) PLATFORMS ruby @@ -368,6 +429,7 @@ DEPENDENCIES rails (>= 6.1.4) rspec-rails rubocop + rubocop-performance sinatra webmock yard diff --git a/event_source.gemspec b/event_source.gemspec index 51b829e2..c361dc3c 100644 --- a/event_source.gemspec +++ b/event_source.gemspec @@ -10,9 +10,9 @@ Gem::Specification.new do |spec| spec.version = EventSource::VERSION spec.authors = ['Dan Thomas'] spec.email = ['info@ideacrew.com'] + spec.metadata['rubygems_mfa_required'] = 'true' - spec.summary = - 'Record changes to application state by storing updates as a sequence of events' + spec.summary = 'Record changes to application state by storing updates as a sequence of events' spec.description = "This service uses Mogoid/MongoDB to create an event object to record a state change and then processes it to update values in the underlying model. It's an implementation of @@ -26,9 +26,7 @@ Gem::Specification.new do |spec| # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) - end + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.bindir = 'exe' @@ -38,15 +36,11 @@ Gem::Specification.new do |spec| spec.add_dependency 'addressable', '>= 2.8.0' spec.add_dependency 'bunny', '>= 2.14' spec.add_dependency 'deep_merge', '~> 1.2.0' - spec.add_dependency 'dry-configurable', '~> 0.12' - spec.add_dependency 'dry-events', '~> 0.3' - spec.add_dependency 'dry-inflector', '~> 0.2' - spec.add_dependency 'dry-initializer', '~> 3.0' - spec.add_dependency 'dry-monads', '~> 1.3' - spec.add_dependency 'dry-struct', '~> 1.4' - spec.add_dependency 'dry-types', '~> 1.5' - spec.add_dependency 'dry-validation', '~> 1.6' - spec.add_dependency 'dry-schema', '~> 1.6' + spec.add_dependency 'dry-configurable', '~> 1.0' + spec.add_dependency 'dry-events', '~> 1.0' + spec.add_dependency 'dry-monads', '~> 1.6' + spec.add_dependency 'dry-struct', '~> 1.6' + spec.add_dependency 'dry-validation', '~> 1.10' spec.add_dependency 'faraday', '~> 1.4.1' spec.add_dependency 'faraday_middleware', '~> 1.0' spec.add_dependency 'logging', '~> 2.3.0' @@ -56,11 +50,4 @@ Gem::Specification.new do |spec| spec.add_dependency 'ox', '~> 2.14' spec.add_dependency 'resque-bus', '~> 0.7.0' spec.add_dependency 'typhoeus', '~> 1.4.0' - - # TODO: Change to development dependency - spec.add_development_dependency 'database_cleaner' - spec.add_development_dependency 'faker' - spec.add_development_dependency 'mongoid' - spec.add_development_dependency 'webmock' - spec.add_development_dependency 'sinatra' end diff --git a/lib/event_source.rb b/lib/event_source.rb index 67615316..d2f0d06a 100644 --- a/lib/event_source.rb +++ b/lib/event_source.rb @@ -2,7 +2,6 @@ require 'forwardable' require 'date' -require 'dry/types/type' require 'dry/monads' require 'dry/monads/do' require 'dry/monads/result' @@ -76,13 +75,11 @@ def config # Call this method on fork of a rails app you are working in. # It cleans up your connections and channels and avoids strange # behaviour. - def reconnect_publishers!; end + def reconnect_publishers! + end def build_async_api_resource(resource) - EventSource::AsyncApi::Operations::AsyncApiConf::Create - .new - .call(resource) - .success + EventSource::AsyncApi::Operations::AsyncApiConf::Create.new.call(resource).success end end From b68580af61067ee7333396a015736f3bdc772ff2 Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Fri, 29 Dec 2023 13:39:41 -0500 Subject: [PATCH 3/4] Add MessageTrait contract and entity Add entities and specs for MessageBinding, Message, Schema and SchemaObject Add Url::Uri#to_s method --- lib/event_source/async_api/async_api.rb | 2 + .../async_api/contracts/message_contract.rb | 4 +- .../contracts/message_trait_contract.rb | 44 +++++++++ lib/event_source/async_api/message.rb | 44 ++++----- lib/event_source/async_api/message_binding.rb | 24 +++++ lib/event_source/async_api/message_trait.rb | 51 ++++------ lib/event_source/async_api/schema.rb | 17 +++- lib/event_source/async_api/schema_object.rb | 43 +++++++++ lib/event_source/async_api/types.rb | 24 +---- lib/event_source/uris/uri.rb | 9 +- .../contracts/message_contract_spec.rb | 7 +- .../contracts/message_trait_contract_spec.rb | 80 ++++++++++++++++ .../async_api/message_binding_spec.rb | 27 ++++++ spec/event_source/async_api/message_spec.rb | 94 +++++++++++++++++++ .../async_api/message_trait_spec.rb | 77 +++++++++++++++ .../async_api/schema_object_spec.rb | 42 +++++++++ spec/event_source/async_api/schema_spec.rb | 31 ++++++ 17 files changed, 530 insertions(+), 90 deletions(-) create mode 100644 lib/event_source/async_api/contracts/message_trait_contract.rb create mode 100644 lib/event_source/async_api/message_binding.rb create mode 100644 lib/event_source/async_api/schema_object.rb create mode 100644 spec/event_source/async_api/contracts/message_trait_contract_spec.rb create mode 100644 spec/event_source/async_api/message_binding_spec.rb create mode 100644 spec/event_source/async_api/message_spec.rb create mode 100644 spec/event_source/async_api/message_trait_spec.rb create mode 100644 spec/event_source/async_api/schema_object_spec.rb create mode 100644 spec/event_source/async_api/schema_spec.rb diff --git a/lib/event_source/async_api/async_api.rb b/lib/event_source/async_api/async_api.rb index ede976c9..823b80f6 100644 --- a/lib/event_source/async_api/async_api.rb +++ b/lib/event_source/async_api/async_api.rb @@ -18,6 +18,7 @@ module AsyncApi require_relative 'error' require_relative 'types' require_relative 'external_documentation' + require_relative 'schema_object' require_relative 'schema' require_relative 'parameter' require_relative 'tag' @@ -25,6 +26,7 @@ module AsyncApi require_relative 'contact' require_relative 'info' require_relative 'message_trait' + require_relative 'message_binding' require_relative 'message' require_relative 'operation_trait' require_relative 'operation' diff --git a/lib/event_source/async_api/contracts/message_contract.rb b/lib/event_source/async_api/contracts/message_contract.rb index 11469e72..9b991362 100644 --- a/lib/event_source/async_api/contracts/message_contract.rb +++ b/lib/event_source/async_api/contracts/message_contract.rb @@ -2,6 +2,8 @@ require 'event_source/async_api/contracts/schema_contract' require 'event_source/async_api/contracts/message_binding_contract' +require 'event_source/async_api/contracts/message_trait_contract' + module EventSource module AsyncApi @@ -40,7 +42,7 @@ class MessageContract < Dry::Validation::Contract optional(:external_docs).array(Types::HashOrNil) optional(:bindings).maybe(EventSource::AsyncApi::Contracts::MessageBindingContract.params) optional(:examples).maybe(Types::HashOrNil) - optional(:traits).array(Types::HashOrNil) + optional(:traits).array(EventSource::AsyncApi::Contracts::MessageTraitContract.params) # Coerce empty attributes with default values before(:value_coercer) do |result| diff --git a/lib/event_source/async_api/contracts/message_trait_contract.rb b/lib/event_source/async_api/contracts/message_trait_contract.rb new file mode 100644 index 00000000..41748168 --- /dev/null +++ b/lib/event_source/async_api/contracts/message_trait_contract.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'event_source/async_api/contracts/schema_contract' +require 'event_source/async_api/contracts/message_binding_contract' + +module EventSource + module AsyncApi + module Contracts + # Schema and validation rules for {EventSource::AsyncApi::Message} + class MessageTraitContract < Dry::Validation::Contract + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [EventSource::AsyncApi::Contracts::SchemaContract] :headers optional + # @option opts [String] :content_type optional + # @option opts [String] :name optional + # @option opts [String] :title optional + # @option opts [String] :summary optional + # @option opts [String] :description optional + # @option opts [Array] :tags optional + # @option opts [ExternalDocumentation] :external_docs optional + # @option opts [EventSource::AsyncApi::Contracts::MessageBindingContract] :bindings optional + # @option opts [Array] :examples optional + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + optional(:headers).maybe(EventSource::AsyncApi::Contracts::SchemaContract.params) + optional(:correlation_id).hash do + optional(:description).maybe(:string) + required(:location).filled(:string) + end + optional(:content_type).maybe(:string) + optional(:name).maybe(:string) + optional(:title).maybe(:string) + optional(:summary).maybe(:string) + optional(:description).maybe(:string) + optional(:tags).array(EventSource::AsyncApi::Contracts::TagContract.params) + optional(:external_docs).array(Types::HashOrNil) + optional(:bindings).maybe(EventSource::AsyncApi::Contracts::MessageBindingContract.params) + optional(:examples).maybe(Types::HashOrNil) + end + end + end + end +end diff --git a/lib/event_source/async_api/message.rb b/lib/event_source/async_api/message.rb index dc28c383..04aa9919 100644 --- a/lib/event_source/async_api/message.rb +++ b/lib/event_source/async_api/message.rb @@ -15,98 +15,86 @@ module AsyncApi # # Describes a message received on a given channel and operation class Message < Dry::Struct - transform_keys(&:to_sym) - # @!attribute [r] headers - # Schema definition of the application headers. Schema must be of type "object". - # It must not define the protocol headers. + # Schema definition of the application headers (it MUST NOT define the protocol headers). # @return [Schema] - attribute :headers, Schema.meta(omittable: true) + attribute? :headers, EventSource::AsyncApi::Schema.meta(omittable: true) # @!attribute [r] payload # Definition of the message payload. It can be of any type but defaults to Schema object # @return [Types::Any] - attribute :payload, Types::Any.meta(omittable: true) + attribute? :payload, Types::Hash.meta(omittable: true) # @!attribute [r] correlation_id # Definition of the correlation ID used for message tracing or matching # @return [String] - attribute :correlation_id do + attribute? :correlation_id do # @!attribute [r] description # An optional description of the identifier. # CommonMark syntax can be used for rich text representation # @return [String] - attribute :description, Types::String.meta(omittable: true) + attribute? :description, Types::String.meta(omittable: true) # @!attribute [r] location # Required. A runtime expression that specifies the location of the correlation ID # @return [String] - attribute :location, Types::String.meta(omittable: true) + attribute :location, Types::String.meta(omittable: false) end.meta(omittable: true) - # @!attribute [r] schema_format - # A string containing the name of the schema format used to define the message payload. - # If omitted, implementations should parse the payload as a Schema object. Check out the - # supported schema formats table for more information. Custom values are allowed but - # their implementation is OPTIONAL. A custom value must not refer to one of the schema formats - # listed in the table. - # @return [String] - attribute :schema_format, Types::String.meta(omittable: true) - # @!attribute [r] content_type # The content type to use when encoding/decoding a message's payload. The value must be a # specific media type (e.g. application/json). When omitted, the value must be the one specified # on the default_content_type field # @return [String] - attribute :contentType, Types::String.meta(omittable: true) + attribute? :content_type, Types::String.meta(omittable: true) # @!attribute [r] name # A machine-friendly name for the message # @return [String] - attribute :name, Types::String.meta(omittable: true) + attribute? :name, Types::String.meta(omittable: true) # @!attribute [r] title # A human-friendly title for the message # @return [String] - attribute :title, Types::String.meta(omittable: true) + attribute? :title, Types::String.meta(omittable: true) # @!attribute [r] summary # A short summary of what the message is about # @return [String] - attribute :summary, Types::String.meta(omittable: true) + attribute? :summary, Types::String.meta(omittable: true) # @!attribute [r] description # A verbose explanation of the message. CommonMark syntax can be used for rich text representation # @return [String] - attribute :description, Types::String.meta(omittable: true) + attribute? :description, Types::String.meta(omittable: true) # @!attribute [r] tags # A list of tags for API documentation control. Tags can be used for logical grouping of messages # @return [Array] - attribute :tags, Types::Array.of(Tag).meta(omittable: true) + attribute? :tags, Types::Array.of(EventSource::AsyncApi::Tag).meta(omittable: true) # @!attribute [r] description # Additional external documentation for this message # @return [ExternalDocumentation] - attribute :external_docs, ExternalDocumentation.meta(omittable: true) + attribute? :external_docs, Types::Array.of(EventSource::AsyncApi::ExternalDocumentation).meta(omittable: true) # @!attribute [r] bindings # Map where the keys describe the name of the protocol and the values describe protocol-specific # definitions for the message # @return [Hash] - attribute :bindings, Types::Hash.meta(omittable: true) + attribute? :bindings, Types::Hash.meta(omittable: true) # @!attribute [r] examples # An array with examples of valid message objects # @return [Hash] - attribute :examples, Types::Array.of(Hash).meta(omittable: true) + attribute? :examples, Types::Array.of(Hash).meta(omittable: true) # @!attribute [r] traits # A list of traits to apply to the message object. Traits must be merged into the message object # using the JSON Merge Patch algorithm in the same order they are defined here. The resulting # object must be a valid Message Object # @return [Array] - attribute :traits, Types::Array.of(MessageTrait).meta(omittable: true) + attribute? :traits, Types::Array.of(EventSource::AsyncApi::MessageTrait).meta(omittable: true) end end end diff --git a/lib/event_source/async_api/message_binding.rb b/lib/event_source/async_api/message_binding.rb new file mode 100644 index 00000000..6e9ec8a7 --- /dev/null +++ b/lib/event_source/async_api/message_binding.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module EventSource + module AsyncApi + # A map where the keys describe the name of the protocol and the values describe protocol-specific definitions + # for the message + class MessageBinding < Dry::Struct + # @!attribute [r] content_encoding + # Returns a MIME encoding for the message content + # @return [String] + attribute? :content_encoding, Types::String.meta(omittable: true) + + # @!attribute [r] message_type + # Returns the application assigned message type + # @return [Types::String] + attribute? :message_type, Types::String.meta(omittable: true) + + # @!attribute [r] binding_version + # Returns the version of this binding. If omitted, "latest" MUST be assumed + # @return [Types::String] + attribute? :binding_version, Types::String.meta(omittable: true) + end + end +end diff --git a/lib/event_source/async_api/message_trait.rb b/lib/event_source/async_api/message_trait.rb index fc0541dc..f26470db 100644 --- a/lib/event_source/async_api/message_trait.rb +++ b/lib/event_source/async_api/message_trait.rb @@ -5,89 +5,74 @@ module AsyncApi # Describes a trait that may be applied to a {Message} object. This object may contain any property # from the Message object, except payload and traits class MessageTrait < Dry::Struct - transform_keys(&:to_sym) - # @!attribute [r] headers - # Schema definition of the application headers. Schema must be of type "object". - # It must not define the protocol headers. - # @return [EventSource::AsyncApi::Schema] - attribute :headers, EventSource::AsyncApi::Schema + # Schema definition of the application headers (it MUST NOT define the protocol headers). + # @return [Schema] + attribute? :headers, EventSource::AsyncApi::Schema.meta(omittable: true) # @!attribute [r] correlation_id # Definition of the correlation ID used for message tracing or matching # @return [String] - attribute :correlation_id do + attribute? :correlation_id do # @!attribute [r] description # An optional description of the identifier. # CommonMark syntax can be used for rich text representation # @return [String] - attribute :description, Types::String + attribute? :description, Types::String.meta(omittable: true) # @!attribute [r] location # Required. A runtime expression that specifies the location of the correlation ID # @return [String] - attribute :location, Types::String - end - - # @!attribute [r] schema_format - # A string containing the name of the schema format used to define the message payload. - # If omitted, implementations should parse the payload as a Schema object. Check out the - # supported schema formats table for more information. Custom values are allowed but - # their implementation is OPTIONAL. A custom value must not refer to one of the schema formats - # listed in the table. - # @return [String] - attribute :schema_format, Types::String + attribute :location, Types::String.meta(omittable: false) + end.meta(omittable: true) # @!attribute [r] content_type # The content type to use when encoding/decoding a message's payload. The value must be a # specific media type (e.g. application/json). When omitted, the value must be the one specified # on the default_content_type field # @return [String] - attribute :content_type, Types::String + attribute? :content_type, Types::String.meta(omittable: true) # @!attribute [r] name # A machine-friendly name for the message # @return [String] - attribute :name, Types::String + attribute? :name, Types::String.meta(omittable: true) # @!attribute [r] title # A human-friendly title for the message # @return [String] - attribute :title, Types::String + attribute? :title, Types::String.meta(omittable: true) # @!attribute [r] summary # A short summary of what the message is about # @return [String] - attribute :summary, Types::String + attribute? :summary, Types::String.meta(omittable: true) # @!attribute [r] description # A verbose explanation of the message. CommonMark syntax can be used for rich text representation # @return [String] - attribute :description, Types::String + attribute? :description, Types::String.meta(omittable: true) # @!attribute [r] tags # A list of tags for API documentation control. Tags can be used for logical grouping of messages - # @return [Array] - attribute :tags, - Types::Array - .of(EventSource::AsyncApi::Tag) - .meta(omittable: true) + # @return [Array] + attribute? :tags, Types::Array.of(EventSource::AsyncApi::Tag).meta(omittable: true) # @!attribute [r] description # Additional external documentation for this message - # @return [EventSource::AsyncApi::ExternalDocumentation] - attribute :external_docs, EventSource::AsyncApi::ExternalDocumentation + # @return [ExternalDocumentation] + attribute? :external_docs, Types::Array.of(EventSource::AsyncApi::ExternalDocumentation).meta(omittable: true) # @!attribute [r] bindings # Map where the keys describe the name of the protocol and the values describe protocol-specific # definitions for the message # @return [Hash] - attribute :bindings, Types::Hash + attribute? :bindings, Types::Hash.meta(omittable: true) # @!attribute [r] examples # An array with examples of valid message objects # @return [Hash] - attribute :examples, Types::Array.of(Hash) + attribute? :examples, Types::Array.of(Hash).meta(omittable: true) end end end diff --git a/lib/event_source/async_api/schema.rb b/lib/event_source/async_api/schema.rb index 20f7fd0f..fd24ac34 100644 --- a/lib/event_source/async_api/schema.rb +++ b/lib/event_source/async_api/schema.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true +require 'event_source/async_api/schema_object' + module EventSource module AsyncApi - # Allows the definition of input and output data types. These types can be objects, - # but also primitives and arrays. This object is a superset of the - # JSON Schema Specification Draft 07 + # A definition of input and output data types. These types can be objects, but also primitives and arrays. class Schema < Dry::Struct + # @!attribute [r] schema_format + # Returns a string containing the name of the schema format used to define the attributes as defined in + # [AsyncApi Schema Format](https://www.asyncapi.com/docs/reference/specification/v3.0.0#multiFormatSchemaObject). + # If omitted, implementations should parse the payload as a Schema object. + # @return [String] + attribute? :schema_format, Types::String.meta(omittable: true) + + # @!attribute [r] schema + # Schema in form of [AsyncApi Schema Object](https://www.asyncapi.com/docs/reference/specification/v3.0.0#schemaObject) + # @return [EventSource::AsyncApi::SchemaObject] + attribute? :schema, EventSource::AsyncApi::SchemaObject.meta(omittable: true) end end end diff --git a/lib/event_source/async_api/schema_object.rb b/lib/event_source/async_api/schema_object.rb new file mode 100644 index 00000000..44ebebf2 --- /dev/null +++ b/lib/event_source/async_api/schema_object.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module EventSource + module AsyncApi + # A definition of input and output data types compliant with + # [AsyncApi Schema Object](https://www.asyncapi.com/docs/reference/specification/v3.0.0#schemaObject) + # @example + # + # { + # type: 'object', + # required: ['name'], + # properties: { + # name: { + # type: 'string' + # }, + # address: { + # '$ref': '#/components/schemas/Address' + # }, + # age: { + # type: 'integer', + # format: 'int32', + # minimum: 0 + # } + # } + # } + class SchemaObject < Dry::Struct + # @!attribute [r] type + # Returns the AsyncApi defined schema type + # @return [Types::String] + attribute :type, Types::String.meta(omittable: false) + + # @!attribute [r] required + # Returns a list of attribute keys that must be included in + # @return [Array] + attribute? :required, Types::Array.of(Types::String).meta(omittable: true) + + # @!attribute [r] properties + # Returns attribute definitions in JSON schema form + # @return [Types::Hash] + attribute? :properties, Types::Hash.meta(omittable: true) + end + end +end diff --git a/lib/event_source/async_api/types.rb b/lib/event_source/async_api/types.rb index e1a97523..622fcf9a 100644 --- a/lib/event_source/async_api/types.rb +++ b/lib/event_source/async_api/types.rb @@ -18,7 +18,7 @@ module Types # end UriKind = Types.Constructor(EventSource::Uris::Uri) do |val| - EventSource::Uris::Uri.new(uri: val) + EventSource::Uris::Uri.new(uri: val).to_s end # UriKind = Types.Constructor(::URI, &:parse) @@ -27,10 +27,7 @@ module Types # TypeContainer = Dry::Schema::TypeContainer.new # TypeContainer.register('params.uri', UriKind) - Email = - Coercible::String.constrained( - format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i - ) + Email = Coercible::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i) Emails = Array.of(Email) HashOrNil = Types::Hash | Types::Nil @@ -66,24 +63,13 @@ module Types :message_bindings ) Vhost = Types::Coercible::String.default('/') - ChannelTypeKind = - Types::Coercible::Symbol - .default(:routing_key) - .enum(:routing_key, :queue) - ExchangeTypeKind = - Types::Coercible::Symbol.enum( - :topic, - :fanout, - :default, - :direct, - :headers - ) + ChannelTypeKind = Types::Coercible::Symbol.default(:routing_key).enum(:routing_key, :queue) + ExchangeTypeKind = Types::Coercible::Symbol.enum(:topic, :fanout, :default, :direct, :headers) MessageDeliveryModeKind = Types::Coercible::Integer.enum(1, 2) RoutingKeyKind = Types::Coercible::String RoutingKeyKinds = Types::Array.of(RoutingKeyKind) QueueName = Types::Coercible::String - AmqpBindingVersionKind = - Types::Coercible::String.default('0.2.0').enum('0.2.0') + AmqpBindingVersionKind = Types::Coercible::String.default('0.2.0').enum('0.2.0') OperationNameType = Types::String | Types::Symbol # PatternedFieldName = String.constrained(format: /^[A-Za-z0-9_\-]+$/) diff --git a/lib/event_source/uris/uri.rb b/lib/event_source/uris/uri.rb index d7ab61f3..e04e622e 100644 --- a/lib/event_source/uris/uri.rb +++ b/lib/event_source/uris/uri.rb @@ -4,8 +4,15 @@ module EventSource module Uris # Class for Uri class Uri + attr_reader :uri + def initialize(uri:) - (uri.is_a? ::URI) ? uri : ::URI.parse(uri) + @uri = uri + (@uri.is_a? ::URI) ? @uri : ::URI.parse(@uri) + end + + def to_s + @uri.to_s end end end diff --git a/spec/event_source/async_api/contracts/message_contract_spec.rb b/spec/event_source/async_api/contracts/message_contract_spec.rb index 611ead94..5358d7d4 100644 --- a/spec/event_source/async_api/contracts/message_contract_spec.rb +++ b/spec/event_source/async_api/contracts/message_contract_spec.rb @@ -49,7 +49,6 @@ let(:tags) { [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] } let(:correlation_id) { { description: 'Default Correlation ID', location: '$message.header#/correlation_id' } } - let(:traits) { [{ '$ref': '#/components/messageTraits/commonHeaders' }] } let(:content_encoding) { 'gzip' } let(:message_type) { 'user.signup' } @@ -59,9 +58,8 @@ { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } end - let(:external_docs) { [] } - let(:bindings) { nil } - let(:examples) { nil } + let(:external_docs) { [{ description: 'Version 1 message', url: 'http://example.com' }] } + let(:traits) { [{ content_type: content_type }] } let(:optional_params) do { @@ -76,7 +74,6 @@ tags: tags, bindings: message_binding, external_docs: external_docs, - examples: examples, traits: traits } end diff --git a/spec/event_source/async_api/contracts/message_trait_contract_spec.rb b/spec/event_source/async_api/contracts/message_trait_contract_spec.rb new file mode 100644 index 00000000..59005b5d --- /dev/null +++ b/spec/event_source/async_api/contracts/message_trait_contract_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Contracts::MessageTraitContract do + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + + let(:header_schema_object_properties) do + { correlation_id: correlation_id_property, occurred_at: occurred_at_property } + end + let(:header_schema_object_type) { 'object' } + let(:header_schema_object_required) { %w[correlation_id] } + + let(:header_schema_object) do + { + type: header_schema_object_type, + required: header_schema_object_required, + properties: header_schema_object_properties + } + end + + let(:header_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:header_schema) { { schema_format: header_schema_format, schema: header_schema_object } } + + let(:content_type) { 'application/json' } + let(:name) { 'UserSignup' } + let(:title) { 'User signup' } + let(:summary) { 'Action to sign a user up.' } + let(:description) { 'A longer description' } + let(:tags) { [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] } + + let(:correlation_id) { { description: 'Default Correlation ID', location: '$message.header#/correlation_id' } } + + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:message_binding) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } + end + + let(:external_docs) { [{ description: 'Version 1 message', url: 'http://example.com' }] } + + + let(:optional_params) do + { + headers: header_schema, + correlation_id: correlation_id, + content_type: content_type, + name: name, + title: title, + summary: summary, + description: description, + tags: tags, + bindings: message_binding, + external_docs: external_docs + } + end + + describe '#call' do + subject(:message_trait) { described_class.new } + + context 'Given empty parameters' do + it 'returns monad success' do + expect(message_trait.call({}).success?).to be_truthy + end + end + + context 'Given optional only parameters' do + it 'returns monad success' do + expect(message_trait.call(optional_params).success?).to be_truthy + end + + it 'all input params are returned' do + expect(message_trait.call(optional_params).to_h).to eq optional_params + end + end + end +end diff --git a/spec/event_source/async_api/message_binding_spec.rb b/spec/event_source/async_api/message_binding_spec.rb new file mode 100644 index 00000000..254c0c81 --- /dev/null +++ b/spec/event_source/async_api/message_binding_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::MessageBinding do + subject(:message_binding) { described_class } + + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:valid_params) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } + end + + context 'Given params that pass contract validation' do + let(:validated_params) { EventSource::AsyncApi::Contracts::MessageBindingContract.new.call(valid_params).to_h } + + it 'it returns an entity instance' do + expect(message_binding.new(validated_params)).to be_a message_binding + end + + it 'and all input params are populated' do + expect(message_binding.new(validated_params).to_h).to eq valid_params + end + end +end diff --git a/spec/event_source/async_api/message_spec.rb b/spec/event_source/async_api/message_spec.rb new file mode 100644 index 00000000..20b85513 --- /dev/null +++ b/spec/event_source/async_api/message_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Message do + subject(:message) { described_class } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + + let(:header_schema_object_properties) do + { correlation_id: correlation_id_property, occurred_at: occurred_at_property } + end + let(:header_schema_object_type) { 'object' } + let(:header_schema_object_required) { %w[correlation_id] } + + let(:header_schema_object) do + { + type: header_schema_object_type, + required: header_schema_object_required, + properties: header_schema_object_properties + } + end + + let(:header_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:header_schema) { { schema_format: header_schema_format, schema: header_schema_object } } + + let(:provider_property) { { type: 'string', description: 'Third party OAuth service that authenticates account' } } + let(:uid_property) { { type: 'string', description: 'Provider-assigned unique account identifier' } } + + let(:payload_schema_object_properties) { { provider: provider_property, uid: uid_property } } + let(:payload_schema_object_type) { 'object' } + let(:payload_schema_object_required) { %w[provider uid] } + + let(:payload_schema_object) do + { + type: payload_schema_object_type, + required: payload_schema_object_required, + properties: payload_schema_object_properties + } + end + + let(:payload_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:payload_schema) { { schema_format: payload_schema_format, schema: payload_schema_object } } + + let(:content_type) { 'application/json' } + let(:name) { 'UserSignup' } + let(:title) { 'User signup' } + let(:summary) { 'Action to sign a user up.' } + let(:description) { 'A longer description' } + let(:tags) { [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] } + + let(:correlation_id) { { description: 'Default Correlation ID', location: '$message.header#/correlation_id' } } + + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:message_binding) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } + end + + let(:external_docs) { [{ description: 'Version 1 message', url: 'http://example.com' }] } + let(:traits) { [{ content_type: content_type }] } + + let(:all_params) do + { + headers: header_schema, + payload: payload_schema, + correlation_id: correlation_id, + content_type: content_type, + name: name, + title: title, + summary: summary, + description: description, + tags: tags, + bindings: message_binding, + external_docs: external_docs, + traits: traits + } + end + + context 'Given validated all params' do + let(:validated_params) { EventSource::AsyncApi::Contracts::MessageContract.new.call(all_params).to_h } + + it 'it returns an entity instance' do + expect(message.new(validated_params)).to be_a message + end + + it 'and all input params are populated' do + expect(message.new(validated_params).to_h).to eq all_params + end + end +end diff --git a/spec/event_source/async_api/message_trait_spec.rb b/spec/event_source/async_api/message_trait_spec.rb new file mode 100644 index 00000000..74a2214f --- /dev/null +++ b/spec/event_source/async_api/message_trait_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::MessageTrait do + subject(:message) { described_class } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + + let(:header_schema_object_properties) do + { correlation_id: correlation_id_property, occurred_at: occurred_at_property } + end + let(:header_schema_object_type) { 'object' } + let(:header_schema_object_required) { %w[correlation_id] } + + let(:header_schema_object) do + { + type: header_schema_object_type, + required: header_schema_object_required, + properties: header_schema_object_properties + } + end + + let(:header_schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + let(:header_schema) { { schema_format: header_schema_format, schema: header_schema_object } } + + let(:provider_property) { { type: 'string', description: 'Third party OAuth service that authenticates account' } } + let(:uid_property) { { type: 'string', description: 'Provider-assigned unique account identifier' } } + + + let(:content_type) { 'application/json' } + let(:name) { 'UserSignup' } + let(:title) { 'User signup' } + let(:summary) { 'Action to sign a user up.' } + let(:description) { 'A longer description' } + let(:tags) { [{ name: 'user' }, { name: 'signup' }, { name: 'register' }] } + + let(:correlation_id) { { description: 'Default Correlation ID', location: '$message.header#/correlation_id' } } + + let(:content_encoding) { 'gzip' } + let(:message_type) { 'user.signup' } + let(:binding_version) { '0.3.0' } + + let(:message_binding) do + { content_encoding: content_encoding, message_type: message_type, binding_version: binding_version } + end + + let(:external_docs) { [{ description: 'Version 1 message', url: 'http://example.com' }] } + + let(:all_params) do + { + headers: header_schema, + correlation_id: correlation_id, + content_type: content_type, + name: name, + title: title, + summary: summary, + description: description, + tags: tags, + bindings: message_binding, + external_docs: external_docs, + } + end + + context 'Given validated all params' do + let(:validated_params) { EventSource::AsyncApi::Contracts::MessageContract.new.call(all_params).to_h } + + it 'it returns an entity instance' do + expect(message.new(validated_params)).to be_a message + end + + it 'and all input params are populated' do + expect(message.new(validated_params).to_h).to eq all_params + end + end +end diff --git a/spec/event_source/async_api/schema_object_spec.rb b/spec/event_source/async_api/schema_object_spec.rb new file mode 100644 index 00000000..b4279d64 --- /dev/null +++ b/spec/event_source/async_api/schema_object_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::SchemaObject do + subject(:schema_object) { described_class } + + let(:type) { 'object' } + let(:required) { %w[correlation_id] } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + let(:properties) { { correlation_id: correlation_id_property, occurred_at: occurred_at_property } } + + let(:required_params) { { type: type } } + let(:optional_params) { { required: required, properties: properties } } + let(:all_params) { required_params.merge(optional_params) } + + context 'Given validated required params' do + let(:validated_params) { EventSource::AsyncApi::Contracts::SchemaObjectContract.new.call(required_params).to_h } + + it 'it returns an entity instance' do + expect(schema_object.new(validated_params)).to be_a schema_object + end + + it 'and all input params are populated' do + expect(schema_object.new(validated_params).to_h).to eq required_params + end + end + + context 'Given validated all params' do + let(:validated_params) { EventSource::AsyncApi::Contracts::SchemaObjectContract.new.call(all_params).to_h } + + it 'it returns an entity instance' do + expect(schema_object.new(validated_params)).to be_a schema_object + end + + it 'and all input params are populated' do + expect(schema_object.new(validated_params).to_h).to eq all_params + end + end +end diff --git a/spec/event_source/async_api/schema_spec.rb b/spec/event_source/async_api/schema_spec.rb new file mode 100644 index 00000000..748a270b --- /dev/null +++ b/spec/event_source/async_api/schema_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EventSource::AsyncApi::Schema do + subject(:schema) { described_class } + + let(:type) { 'object' } + let(:required) { %w[correlation_id] } + + let(:occurred_at_property) { { type: 'string', description: 'Message timestamp' } } + let(:correlation_id_property) { { type: 'string', description: 'Correlation ID set by application' } } + let(:properties) { { correlation_id: correlation_id_property, occurred_at: occurred_at_property } } + let(:schema_object) { { type: type, required: required, properties: properties } } + + let(:schema_format) { 'application/vnd.apache.avro+json;version=1.9.0' } + + let(:valid_params) { { schema_format: schema_format, schema: schema_object } } + + context 'Given params that pass contract validation' do + let(:validated_params) { EventSource::AsyncApi::Contracts::SchemaContract.new.call(valid_params).to_h } + + it 'it returns an entity instance' do + expect(schema.new(validated_params)).to be_a schema + end + + it 'and all input params are populated' do + expect(schema.new(validated_params).to_h).to eq valid_params + end + end +end From 07b3fa473c35405c8e46ef83b71c1433ef9f441a Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Wed, 2 Jul 2025 16:55:06 -0400 Subject: [PATCH 4/4] Update .yardopts, add CHANGELOG, enhance CODE_OF_CONDUCT, and update LICENSE year --- .yardopts | 19 +++-- HISTORY.md => CHANGELOG.md | 0 CODE_OF_CONDUCT.md | 155 +++++++++++++++++++++++++------------ LICENSE.txt | 2 +- 4 files changed, 118 insertions(+), 58 deletions(-) rename HISTORY.md => CHANGELOG.md (100%) diff --git a/.yardopts b/.yardopts index d846e0a9..67a61d2b 100644 --- a/.yardopts +++ b/.yardopts @@ -1,9 +1,16 @@ ---no-private ---protected ---markup="markdown" lib/**/*.rb --main README.md ---hide-tag todo +--output-dir doc lib/**/*.rb +--markup=markdown +--markup-provider=redcarpet +--private +--protected +--embed-mixin ClassMethods + +--tag nodocs --query '!@nodocs' +--tag http:"HTTP Status" +--tag url:"Example Endpoints" + - +CHANGELOG.md LICENSE.txt -ChangeLog.md -CODE_OF_CONDUCT.md \ No newline at end of file +CODE_OF_CONDUCT.md diff --git a/HISTORY.md b/CHANGELOG.md similarity index 100% rename from HISTORY.md rename to CHANGELOG.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2874bb53..e7a87c3c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,74 +1,127 @@ + # Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. -Examples of behavior that contributes to creating a positive environment -include: +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: +## Our Standards -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at dan@ideacrew.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at [info@ideacrew.com](mailto:info@ideacrew.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [https://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, +available at +. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). -[homepage]: https://contributor-covenant.org -[version]: https://contributor-covenant.org/version/1/4/ +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/LICENSE.txt b/LICENSE.txt index 27b1a6fb..52555bf6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019-2021 IdeaCrew, Inc +Copyright (c) 2019-2025 IdeaCrew, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal