├── .gitignore ├── Gemfile ├── MIT-License.txt ├── README.md ├── init.rb ├── install-gems.sh ├── lib ├── message_store.rb └── message_store │ ├── controls.rb │ ├── controls │ ├── category.rb │ ├── get.rb │ ├── get_last.rb │ ├── id.rb │ ├── message_data.rb │ ├── message_data │ │ ├── hash.rb │ │ ├── metadata.rb │ │ ├── read.rb │ │ └── write.rb │ ├── random_value.rb │ ├── read.rb │ ├── stream_name.rb │ ├── time.rb │ └── write.rb │ ├── expected_version.rb │ ├── get.rb │ ├── get │ ├── stream │ │ ├── last.rb │ │ └── last │ │ │ └── substitute.rb │ └── substitute.rb │ ├── id.rb │ ├── log.rb │ ├── message_data.rb │ ├── message_data │ ├── hash │ │ └── transform.rb │ ├── read.rb │ └── write.rb │ ├── no_stream.rb │ ├── read.rb │ ├── read │ └── iterator.rb │ ├── stream_name.rb │ └── write.rb ├── library-symlinks.sh ├── load_path.rb ├── message_store.gemspec ├── remove-lib-symlinks.sh ├── symlink-lib.sh ├── test.rb └── test ├── automated.rb ├── automated ├── automated_init.rb ├── get_last │ ├── call.rb │ ├── configure.rb │ └── substitute.rb ├── id │ ├── compose.rb │ ├── get_cardinal_id.rb │ └── parse.rb ├── iterator │ ├── batch_depletion │ │ ├── depleted │ │ │ ├── batch_index_is_batch_length.rb │ │ │ ├── batch_is_empty.rb │ │ │ └── batch_is_nil.rb │ │ └── not_depleted │ │ │ └── batch_index_less_than_batch_length.rb │ ├── batch_initialization.rb │ ├── next │ │ ├── category │ │ │ ├── next.rb │ │ │ └── next_from_position.rb │ │ └── stream │ │ │ ├── next.rb │ │ │ └── next_from_position.rb │ ├── no_further_message_data.rb │ └── stream_depletion │ │ ├── depleted │ │ └── resulting_batch_size_less_than_requested_batch_size.rb │ │ └── not_depleted │ │ ├── batch_is_uninitialized.rb │ │ ├── resulting_batch_size_equal_to_requested_batch_size.rb │ │ └── resulting_batch_size_greater_than_requested_batch_size.rb ├── iterator_tests.rb ├── message_data │ ├── case_equality.rb │ └── hash.rb ├── read │ ├── asynchronous_result.rb │ └── missing_block_error.rb ├── stream_name │ ├── category_predicate.rb │ ├── compose │ │ ├── cardinal_id.rb │ │ ├── category.rb │ │ ├── compound_id.rb │ │ ├── missing_category_error.rb │ │ ├── singular_ids.rb │ │ ├── stream_name.rb │ │ └── types.rb │ └── parse │ │ ├── get_cardinal_id.rb │ │ ├── get_category.rb │ │ ├── get_category_type.rb │ │ ├── get_category_types.rb │ │ ├── get_entity_name.rb │ │ ├── get_id.rb │ │ └── get_ids.rb └── write │ └── ids_are_assigned.rb └── test_init.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | Gemfile.lock 3 | *.log 4 | *.gem 5 | gems 6 | 7 | lib/message_store/event_store 8 | lib/message_store/event_store.rb 9 | lib/message_store/postgres 10 | lib/message_store/postgres.rb 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /MIT-License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Scott Bellware 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # message_store 2 | 3 | Common primitives for platform-specific message store implementations. 4 | 5 | ## Documentation 6 | 7 | See the [Eventide documentation site](http://docs.eventide-project.org) for more information, examples, and user guides. 8 | 9 | ## License 10 | 11 | The `event_stream-postgres` library is released under the [MIT License](https://github.com/eventide-project/event-stream-postgres/blob/master/MIT-License.txt). 12 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require_relative 'load_path' 2 | 3 | require 'message_store' 4 | -------------------------------------------------------------------------------- /install-gems.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ -z ${POSTURE+x} ]; then 6 | echo "(POSTURE is not set. Using \"operational\" by default.)" 7 | posture="operational" 8 | else 9 | posture=$POSTURE 10 | fi 11 | 12 | echo 13 | echo "Installing gems locally (posture: $posture)" 14 | echo '= = =' 15 | 16 | cmd="bundle install --standalone --path=./gems" 17 | 18 | if [ operational == "$posture" ]; then 19 | cmd="$cmd --without=development" 20 | fi 21 | 22 | echo $cmd 23 | ($cmd) 24 | 25 | echo '- - -' 26 | echo '(done)' 27 | echo 28 | -------------------------------------------------------------------------------- /lib/message_store.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | require 'json' 3 | 4 | require 'casing' 5 | require 'identifier/uuid' 6 | require 'schema' 7 | require 'initializer' 8 | require 'transform' 9 | require 'template_method' 10 | require 'async_invocation' 11 | 12 | require 'message_store/expected_version' 13 | require 'message_store/no_stream' 14 | require 'message_store/id' 15 | require 'message_store/stream_name' 16 | 17 | require 'message_store/message_data' 18 | require 'message_store/message_data/hash/transform' 19 | require 'message_store/message_data/write' 20 | require 'message_store/message_data/read' 21 | 22 | require 'message_store/log' 23 | 24 | require 'message_store/get' 25 | require 'message_store/get/substitute' 26 | require 'message_store/get/stream/last' 27 | require 'message_store/get/stream/last/substitute' 28 | require 'message_store/read/iterator' 29 | require 'message_store/read' 30 | require 'message_store/write' 31 | -------------------------------------------------------------------------------- /lib/message_store/controls.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | require 'clock/controls' 4 | require 'identifier/uuid/controls' 5 | 6 | require 'message_store/controls/random_value' 7 | require 'message_store/controls/time' 8 | require 'message_store/controls/id' 9 | require 'message_store/controls/category' 10 | require 'message_store/controls/stream_name' 11 | require 'message_store/controls/read' 12 | require 'message_store/controls/message_data' 13 | require 'message_store/controls/message_data/hash' 14 | require 'message_store/controls/message_data/metadata' 15 | require 'message_store/controls/message_data/write' 16 | require 'message_store/controls/message_data/read' 17 | require 'message_store/controls/write' 18 | require 'message_store/controls/get' 19 | require 'message_store/controls/get_last' 20 | -------------------------------------------------------------------------------- /lib/message_store/controls/category.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module Category 4 | def self.example(category: nil, type: nil, types: nil, randomize_category: nil) 5 | if randomize_category.nil? 6 | if !category.nil? 7 | randomize_category = false 8 | end 9 | end 10 | 11 | randomize_category = true if randomize_category.nil? 12 | 13 | category ||= 'test' 14 | 15 | if randomize_category 16 | category = "#{category}#{SecureRandom.hex(16)}XX" 17 | end 18 | 19 | category = Controls::StreamName.stream_name(category, type: type, types: types) 20 | 21 | category 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/message_store/controls/get.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module Get 4 | def self.example(stream_name: nil, batch_size: nil, count: nil, global_position_offset: nil) 5 | stream_name ||= StreamName.example 6 | batch_size ||= 1 7 | count ||= 1 8 | global_position_offset ||= -> (x) { x ** 2 } 9 | 10 | get = MessageStore::Get::Substitute.build 11 | get.stream_name = stream_name 12 | get.batch_size = batch_size 13 | 14 | elements = (0..(count - 1)).to_a 15 | 16 | elements.each do |e| 17 | message_data = MessageData::Read.example 18 | message_data.position = e 19 | message_data.global_position = global_position_offset.(message_data.position) 20 | 21 | get.items << message_data 22 | end 23 | 24 | get 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/message_store/controls/get_last.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module GetLast 4 | def self.example 5 | Example.build 6 | end 7 | 8 | def self.default_session 9 | :default_session 10 | end 11 | 12 | class Example 13 | include MessageStore::Get::Stream::Last 14 | 15 | attr_writer :session 16 | def session 17 | @session ||= GetLast.default_session 18 | end 19 | 20 | def call(stream_name) 21 | end 22 | 23 | def configure(session: nil) 24 | self.session = session 25 | end 26 | 27 | def session?(session) 28 | self.session.equal?(session) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/message_store/controls/id.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module ID 4 | def self.example(i=nil, increment: nil, sample: nil) 5 | Identifier::UUID::Controls::Incrementing.example(i=nil, increment: nil, sample: nil) 6 | end 7 | 8 | Random = Identifier::UUID::Controls::Random 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/message_store/controls/message_data.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module MessageData 4 | def self.id 5 | ID::Random.example 6 | end 7 | 8 | def self.type 9 | 'SomeType' 10 | end 11 | 12 | def self.other_type 13 | 'SomeOtherType' 14 | end 15 | 16 | def self.data 17 | { :attribute => RandomValue.example } 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/message_store/controls/message_data/hash.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module MessageData 4 | module Hash 5 | def self.data 6 | { 7 | some_attribute: 'some value' 8 | } 9 | end 10 | 11 | def self.example 12 | MessageStore::MessageData::Hash[data] 13 | end 14 | 15 | module JSON 16 | def self.data(id=nil) 17 | data = Hash.data 18 | Casing::Camel.(data, symbol_to_string: true) 19 | end 20 | 21 | def self.text 22 | ::JSON.generate(data) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/message_store/controls/message_data/metadata.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module MessageData 4 | module Metadata 5 | def self.data 6 | { 7 | meta_attribute: RandomValue.example 8 | } 9 | end 10 | 11 | module JSON 12 | def self.data(id=nil) 13 | data = Metadata.data 14 | Casing::Camel.(data, symbol_to_string: true) 15 | end 16 | 17 | def self.text 18 | data.to_json 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/message_store/controls/message_data/read.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module MessageData 4 | module Read 5 | def self.example(id: nil, type: nil, data: nil, metadata: nil) 6 | if id == :none 7 | id = nil 8 | else 9 | id ||= self.id 10 | end 11 | 12 | type ||= self.type 13 | 14 | if data == :none 15 | data = nil 16 | else 17 | data ||= self.data 18 | end 19 | 20 | if metadata == :none 21 | metadata = nil 22 | else 23 | metadata ||= self.metadata 24 | end 25 | 26 | message_data = MessageStore::MessageData::Read.build 27 | 28 | message_data.id = id 29 | message_data.type = type 30 | message_data.data = data 31 | message_data.metadata = metadata 32 | message_data.position = position 33 | message_data.global_position = global_position 34 | message_data.time = time 35 | message_data.stream_name = stream_name 36 | 37 | message_data 38 | end 39 | 40 | def self.id 41 | MessageData.id 42 | end 43 | 44 | def self.type 45 | MessageData.type 46 | end 47 | 48 | def self.data 49 | MessageData.data 50 | end 51 | 52 | def self.metadata 53 | MessageData::Metadata.data 54 | end 55 | 56 | def self.position 57 | 1 58 | end 59 | 60 | def self.global_position 61 | 111 62 | end 63 | 64 | def self.time 65 | Time::Raw.example 66 | end 67 | 68 | def self.stream_name 69 | StreamName.example 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/message_store/controls/message_data/write.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module MessageData 4 | module Write 5 | def self.example(id: nil, type: nil, data: nil, metadata: nil) 6 | if id == :none 7 | id = nil 8 | else 9 | id ||= self.id 10 | end 11 | 12 | type ||= self.type 13 | 14 | if data == :none 15 | data = nil 16 | else 17 | data ||= self.data 18 | end 19 | 20 | if metadata == :none 21 | metadata = nil 22 | else 23 | metadata ||= self.metadata 24 | end 25 | 26 | message_data = MessageStore::MessageData::Write.build 27 | 28 | message_data.id = id 29 | message_data.type = type 30 | message_data.data = data 31 | message_data.metadata = metadata 32 | 33 | message_data 34 | end 35 | 36 | def self.id 37 | MessageData.id 38 | end 39 | 40 | def self.type 41 | MessageData.type 42 | end 43 | 44 | def self.data 45 | MessageData.data 46 | end 47 | 48 | def self.metadata 49 | MessageData::Metadata.data 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/message_store/controls/random_value.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module RandomValue 4 | def self.example 5 | SecureRandom.hex 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/message_store/controls/read.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module Read 4 | def self.example(stream_name: nil) 5 | stream_name ||= StreamName.example 6 | 7 | read = Example.build(stream_name) 8 | 9 | get = Get.example(stream_name: stream_name) 10 | read.iterator.get = get 11 | 12 | read 13 | end 14 | 15 | class Example 16 | include MessageStore::Read 17 | 18 | def configure(*); end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/message_store/controls/stream_name.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module StreamName 4 | def self.example(category: nil, id: nil, type: nil, types: nil, randomize_category: nil) 5 | if id == :none 6 | id = nil 7 | else 8 | id ||= Identifier::UUID.random 9 | end 10 | 11 | category = Category.example(category: category, randomize_category: randomize_category) 12 | 13 | stream_name(category, id, type: type, types: types) 14 | end 15 | 16 | def self.stream_name(category, id=nil, type: nil, types: nil) 17 | types = Array(types) 18 | types.unshift(type) unless type.nil? 19 | 20 | type_list = nil 21 | type_list = types.join('+') unless types.empty? 22 | 23 | stream_name = category 24 | stream_name = "#{stream_name}:#{type_list}" unless type_list.nil? 25 | 26 | if not id.nil? 27 | composed_id = MessageStore::ID.id(id) 28 | stream_name = "#{stream_name}-#{composed_id}" 29 | end 30 | 31 | stream_name 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/message_store/controls/time.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module Time 4 | def self.example(time=nil) 5 | time ||= Raw.example 6 | ISO8601.example(time) 7 | end 8 | 9 | module Raw 10 | def self.example 11 | Clock::Controls::Time::Raw.example 12 | end 13 | end 14 | 15 | module ISO8601 16 | def self.example(time=nil) 17 | Clock::Controls::Time::ISO8601.example(time) 18 | end 19 | 20 | def self.precision 21 | 3 22 | end 23 | end 24 | 25 | module Processed 26 | def self.example(time=nil, offset_milliseconds: nil) 27 | offset_milliseconds ||= self.offset_milliseconds 28 | Clock::Controls::Time::Offset.example(offset_milliseconds, time: time, precision: ISO8601.precision) 29 | end 30 | 31 | module Raw 32 | def self.example(time=nil, offset_milliseconds: nil) 33 | offset_milliseconds ||= Processed.offset_milliseconds 34 | Clock::Controls::Time::Offset::Raw.example(offset_milliseconds, time: time, precision: ISO8601.precision) 35 | end 36 | end 37 | 38 | def self.offset_milliseconds 39 | 11 40 | end 41 | end 42 | 43 | module Effective 44 | def self.example(time=nil, offset_milliseconds: nil) 45 | offset_milliseconds ||= self.offset_milliseconds 46 | Clock::Controls::Time::Offset.example(offset_milliseconds, time: time, precision: ISO8601.precision) 47 | end 48 | 49 | module Raw 50 | def self.example(time=nil, offset_milliseconds: nil) 51 | offset_milliseconds ||= Effective.offset_milliseconds 52 | Clock::Controls::Time::Offset::Raw.example(offset_milliseconds, time: time, precision: ISO8601.precision) 53 | end 54 | end 55 | 56 | def self.offset_milliseconds 57 | 1 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/message_store/controls/write.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Controls 3 | module Write 4 | def self.example 5 | Example.build 6 | end 7 | 8 | class Example 9 | include MessageStore::Write 10 | 11 | def configure(*) 12 | end 13 | 14 | def write(batch, stream_name, expected_version: nil) 15 | logger.trace { "Writing batch (Stream Name: #{stream_name}, Number of Events: #{batch.length}, Expected Version: #{expected_version.inspect})" } 16 | logger.debug { "Wrote batch (Stream Name: #{stream_name}, Number of Events: #{batch.length}, Expected Version: #{expected_version.inspect})" } 17 | 18 | nil 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/message_store/expected_version.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module ExpectedVersion 3 | Error = Class.new(RuntimeError) 4 | 5 | def self.canonize(expected_version) 6 | return nil if expected_version.nil? 7 | return expected_version unless expected_version == NoStream.name 8 | NoStream.version 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/message_store/get.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Get 3 | def self.included(cls) 4 | cls.class_exec do 5 | include Dependency 6 | include Initializer 7 | include TemplateMethod 8 | include Log::Dependency 9 | 10 | template_method! :call 11 | template_method! :stream_name 12 | template_method! :batch_size 13 | template_method! :last_position 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/message_store/get/stream/last.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Get 3 | module Stream 4 | module Last 5 | def self.included(cls) 6 | cls.class_exec do 7 | include Dependency 8 | include TemplateMethod 9 | include Log::Dependency 10 | 11 | extend Build 12 | extend Call 13 | extend Configure 14 | 15 | prepend InstanceActuator 16 | 17 | template_method :configure 18 | template_method! :call 19 | 20 | const_set :Substitute, Substitute 21 | end 22 | end 23 | 24 | module InstanceActuator 25 | def call(stream_name, type=nil) 26 | logger.trace(tag: :get) { "Getting last message data (Stream Name: #{stream_name})" } 27 | 28 | message_data = super 29 | 30 | logger.info(tag: :get) { "Finished getting message data (Stream Name: #{stream_name})" } 31 | logger.info(tags: [:data, :message_data]) { message_data.pretty_inspect } 32 | 33 | message_data 34 | end 35 | end 36 | 37 | module Build 38 | def build(session: nil) 39 | instance = new 40 | instance.configure(session: session) 41 | instance 42 | end 43 | end 44 | 45 | module Configure 46 | def configure(receiver, session: nil, attr_name: nil) 47 | attr_name ||= :get_last 48 | 49 | instance = build(session: session) 50 | receiver.public_send("#{attr_name}=", instance) 51 | instance 52 | end 53 | end 54 | 55 | module Call 56 | def call(stream_name, type=nil, session: nil) 57 | instance = build(session: session) 58 | instance.(stream_name, type) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/message_store/get/stream/last/substitute.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Get 3 | module Stream 4 | module Last 5 | module Substitute 6 | def self.build 7 | GetLast.new 8 | end 9 | 10 | class GetLast 11 | include Get::Stream::Last 12 | 13 | def call(stream_name, type=nil) 14 | streams[stream_name] 15 | end 16 | 17 | def set(stream_name, message_data) 18 | streams[stream_name] = message_data 19 | end 20 | 21 | def streams 22 | @streams ||= {} 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/message_store/get/substitute.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Get 3 | class Substitute 4 | include Initializer 5 | include TemplateMethod 6 | 7 | include Get 8 | 9 | attr_accessor :stream_name 10 | alias :category :stream_name 11 | 12 | def batch_size 13 | @batch_size ||= 1 14 | end 15 | attr_writer :batch_size 16 | 17 | def items 18 | @items ||= [] 19 | end 20 | 21 | def self.build 22 | new 23 | end 24 | 25 | def call(position) 26 | position ||= 0 27 | 28 | logger.trace(tag: :get) { "Getting (Position: #{position}, Stream Name: #{stream_name.inspect}, Batch Size: #{batch_size})" } 29 | 30 | logger.debug(tag: :data) { "Items: \n#{items.pretty_inspect}" } 31 | logger.debug(tag: :data) { "Position: #{position.inspect}" } 32 | logger.debug(tag: :data) { "Batch Size: #{batch_size.inspect}" } 33 | 34 | # No specialized Gets for substitute 35 | # Complexity has to be inline for the control 36 | # Scott, Tue Oct 1 2019 37 | unless self.class.category_stream?(stream_name) 38 | index = (items.index { |i| i.position >= position }) 39 | else 40 | index = (items.index { |i| i.global_position >= position }) 41 | end 42 | 43 | logger.debug(tag: :data) { "Index: #{index.inspect}" } 44 | 45 | if index.nil? 46 | items = [] 47 | else 48 | range = index..(index + batch_size - 1) 49 | logger.debug(tag: :data) { "Range: #{range.pretty_inspect}" } 50 | 51 | items = self.items[range] 52 | end 53 | 54 | logger.info(tag: :data) { "Got: \n#{items.pretty_inspect}" } 55 | logger.info(tag: :get) { "Finished getting (Position: #{position}, Stream Name: #{stream_name.inspect})" } 56 | 57 | items 58 | end 59 | 60 | def last_position(batch) 61 | if self.class.category_stream?(stream_name) 62 | batch.last.global_position 63 | else 64 | batch.last.position 65 | end 66 | end 67 | 68 | def self.category_stream?(stream_name) 69 | StreamName.category?(stream_name) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/message_store/id.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module ID 3 | Error = Class.new(RuntimeError) 4 | 5 | def self.compound_id_separator 6 | '+' 7 | end 8 | 9 | def self.id(id) 10 | if id.is_a?(Array) 11 | id = compound_id(id) 12 | else 13 | if id.nil? 14 | raise Error, "ID must not be omitted" 15 | end 16 | end 17 | 18 | id 19 | end 20 | 21 | def self.compound_id(ids) 22 | if ids.empty? 23 | raise Error, "IDs must not be omitted" 24 | end 25 | 26 | ids.join(compound_id_separator) 27 | end 28 | 29 | def self.get_cardinal_id(id) 30 | parse(id).first 31 | end 32 | 33 | def self.parse(id) 34 | if id.nil? 35 | raise Error, "ID must not be omitted" 36 | end 37 | 38 | id.split(compound_id_separator) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/message_store/log.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | class Log < ::Log 3 | def tag!(tags) 4 | tags << :message_store 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/message_store/message_data.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module MessageData 3 | def self.included(cls) 4 | cls.class_exec do 5 | include Schema::DataStructure 6 | 7 | attribute :id, String 8 | attribute :type, String 9 | attribute :data, ::Hash 10 | attribute :metadata, ::Hash 11 | 12 | def ===(other) 13 | type == other 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/message_store/message_data/hash/transform.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module MessageData 3 | class Hash < ::Hash 4 | module Transform 5 | def self.json 6 | JSON 7 | end 8 | 9 | def self.instance(raw_data) 10 | Hash[raw_data] 11 | end 12 | 13 | def self.raw_data(instance) 14 | Hash[instance] 15 | end 16 | 17 | module JSON 18 | def self.write(raw_hash_data) 19 | json_formatted_data = Casing::Camel.(raw_hash_data) 20 | ::JSON.generate(json_formatted_data) 21 | end 22 | 23 | def self.read(text) 24 | json_formatted_data = ::JSON.parse(text, :symbolize_names => true) 25 | Casing::Underscore.(json_formatted_data) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/message_store/message_data/read.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module MessageData 3 | class Read 4 | include MessageData 5 | 6 | attribute :stream_name, String 7 | attribute :position, Integer 8 | attribute :global_position, Integer 9 | attribute :time, Time 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/message_store/message_data/write.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module MessageData 3 | class Write 4 | include MessageData 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/message_store/no_stream.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module NoStream 3 | def self.name 4 | :no_stream 5 | end 6 | 7 | def self.version 8 | -1 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/message_store/read.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Read 3 | def self.included(cls) 4 | cls.class_exec do 5 | include Dependency 6 | include Initializer 7 | include TemplateMethod 8 | include Log::Dependency 9 | 10 | extend Build 11 | extend Call 12 | extend Configure 13 | 14 | dependency :iterator, Iterator 15 | 16 | initializer :stream_name, :position, :batch_size 17 | 18 | template_method! :configure 19 | end 20 | end 21 | 22 | Error = Class.new(RuntimeError) 23 | 24 | module Build 25 | def build(stream_name, position: nil, batch_size: nil, session: nil, **arguments) 26 | new(stream_name, position, batch_size).tap do |instance| 27 | Iterator.configure(instance, position) 28 | instance.configure(session: session, **arguments) 29 | end 30 | end 31 | end 32 | 33 | module Call 34 | def call(stream_name, position: nil, batch_size: nil, session: nil, **arguments, &action) 35 | instance = build(stream_name, position: position, batch_size: batch_size, session: session, **arguments) 36 | instance.(&action) 37 | end 38 | end 39 | 40 | module Configure 41 | def configure(receiver, stream_name, attr_name: nil, position: nil, batch_size: nil, session: nil, **arguments) 42 | attr_name ||= :read 43 | instance = build(stream_name, position: position, batch_size: batch_size, session: session, **arguments) 44 | receiver.public_send "#{attr_name}=", instance 45 | end 46 | end 47 | 48 | def call(&action) 49 | logger.trace(tag: :read) { "Reading (Stream Name: #{stream_name})" } 50 | 51 | if action.nil? 52 | error_message = "Reader must be actuated with a block" 53 | logger.error(tag: :read) { error_message } 54 | raise Error, error_message 55 | end 56 | 57 | enumerate_message_data(&action) 58 | 59 | logger.info(tag: :read) { "Reading completed (Stream Name: #{stream_name})" } 60 | 61 | return AsyncInvocation::Incorrect 62 | end 63 | 64 | def enumerate_message_data(&action) 65 | logger.trace(tag: :read) { "Enumerating (Stream Name: #{stream_name})" } 66 | 67 | message_data = nil 68 | 69 | loop do 70 | message_data = iterator.next 71 | 72 | break if message_data.nil? 73 | logger.debug(tags: [:data, :message_data]) { message_data.pretty_inspect } 74 | 75 | action.(message_data) 76 | end 77 | 78 | logger.debug(tag: :read) { "Enumerated (Stream Name: #{stream_name})" } 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/message_store/read/iterator.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Read 3 | class Iterator 4 | include Dependency 5 | include Initializer 6 | include TemplateMethod 7 | include Log::Dependency 8 | 9 | dependency :get, Get 10 | 11 | attr_accessor :batch 12 | 13 | def starting_position 14 | @starting_position ||= 0 15 | end 16 | attr_writer :starting_position 17 | 18 | def batch_index 19 | @batch_index ||= 0 20 | end 21 | attr_writer :batch_index 22 | 23 | def batch_size 24 | get.batch_size 25 | end 26 | 27 | def self.build(position=nil) 28 | new.tap do |instance| 29 | instance.starting_position = position 30 | Log.get(self).debug { "Built Iterator (Starting Position: #{position.inspect})" } 31 | end 32 | end 33 | 34 | def self.configure(receiver, position=nil, attr_name: nil) 35 | attr_name ||= :iterator 36 | instance = build(position) 37 | receiver.public_send "#{attr_name}=", instance 38 | end 39 | 40 | def next 41 | logger.trace { "Getting next message data (Batch Length: #{(batch &.length).inspect}, Batch Index: #{batch_index})" } 42 | 43 | if batch_depleted? 44 | resupply 45 | end 46 | 47 | message_data = batch[batch_index] 48 | 49 | logger.debug(tags: [:data, :message_data]) { "Next message data: #{message_data.pretty_inspect}" } 50 | logger.debug { "Done getting next message data (Batch Length: #{(batch &.length).inspect}, Batch Index: #{batch_index})" } 51 | 52 | advance_batch_index 53 | 54 | message_data 55 | end 56 | 57 | def resupply 58 | logger.trace { "Resupplying batch (Current Batch Length: #{(batch &.length).inspect})" } 59 | 60 | batch = [] 61 | unless stream_depleted? 62 | batch = get_batch 63 | end 64 | 65 | reset(batch) 66 | 67 | logger.debug { "Batch resupplied (Next Batch Length: #{(batch &.length).inspect})" } 68 | end 69 | 70 | def get_batch 71 | position = next_batch_starting_position 72 | 73 | logger.trace "Getting batch (Position: #{position.inspect})" 74 | 75 | batch = get.(position) 76 | 77 | logger.debug { "Finished getting batch (Count: #{batch.length}, Position: #{position.inspect})" } 78 | 79 | batch 80 | end 81 | 82 | def next_batch_starting_position 83 | if not batch_initialized? 84 | logger.debug { "Batch is not initialized (Next batch starting position: #{starting_position.inspect})" } 85 | return starting_position 86 | end 87 | 88 | previous_position = last_position 89 | next_position = previous_position + 1 90 | logger.debug { "End of batch (Next starting position: #{next_position}, Previous Position: #{previous_position})" } 91 | 92 | next_position 93 | end 94 | 95 | def last_position 96 | get.last_position(batch) 97 | end 98 | 99 | def reset(batch) 100 | logger.trace { "Resetting batch" } 101 | 102 | self.batch = batch 103 | self.batch_index = 0 104 | 105 | logger.debug(tags: [:data, :batch]) { "Batch set to: \n#{batch.pretty_inspect}" } 106 | logger.debug(tags: [:data, :batch]) { "Batch position set to: #{batch_index.inspect}" } 107 | logger.debug { "Done resetting batch" } 108 | end 109 | 110 | def advance_batch_index 111 | logger.trace { "Advancing batch index (Batch Index: #{batch_index})" } 112 | self.batch_index += 1 113 | logger.debug { "Advanced batch index (Batch Index: #{batch_index})" } 114 | end 115 | 116 | def batch_initialized? 117 | not batch.nil? 118 | end 119 | 120 | def batch_depleted? 121 | if not batch_initialized? 122 | logger.debug { "Batch is depleted (Batch is not initialized)" } 123 | return true 124 | end 125 | 126 | if batch.empty? 127 | logger.debug { "Batch is depleted (Batch is empty)" } 128 | return true 129 | end 130 | 131 | if batch_index == batch.length 132 | logger.debug { "Batch is depleted (Batch Index: #{batch_index}, Batch Length: #{batch.length})" } 133 | return true 134 | end 135 | 136 | logger.debug { "Batch is not depleted (Batch Index: #{batch_index}, Batch Length: #{batch.length})" } 137 | false 138 | end 139 | 140 | def stream_depleted? 141 | if not batch_initialized? 142 | logger.debug { "Stream is not depleted (Batch Length: (batch is nil), Batch Size: #{batch_size})" } 143 | return false 144 | end 145 | 146 | if batch.length < batch_size 147 | logger.debug { "Stream is depleted (Batch Length: #{batch.length}, Batch Size: #{batch_size})" } 148 | return true 149 | end 150 | 151 | logger.debug { "Stream is not depleted (Batch Length: #{batch.length}, Batch Size: #{batch_size})" } 152 | false 153 | end 154 | 155 | ## Need not exist? 156 | class Substitute < Iterator 157 | ## include Read::Iterator 158 | 159 | def self.build() 160 | new 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/message_store/stream_name.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module StreamName 3 | Error = Class.new(RuntimeError) 4 | 5 | def self.id_separator 6 | '-' 7 | end 8 | 9 | def self.compound_id_separator 10 | ID.compound_id_separator 11 | end 12 | 13 | def self.category_type_separator 14 | ':' 15 | end 16 | 17 | def self.compound_type_separator 18 | '+' 19 | end 20 | 21 | def self.stream_name(category, stream_id=nil, cardinal_id: nil, id: nil, ids: nil, type: nil, types: nil) 22 | if category == nil 23 | raise Error, "Category must not be omitted from stream name" 24 | end 25 | 26 | stream_name = category 27 | 28 | type_list = [] 29 | type_list.concat(Array(type)) 30 | type_list.concat(Array(types)) 31 | 32 | type_part = type_list.join(compound_type_separator) 33 | 34 | if not type_part.empty? 35 | stream_name = "#{stream_name}#{category_type_separator}#{type_part}" 36 | end 37 | 38 | id_list = [] 39 | id_list << cardinal_id if not cardinal_id.nil? 40 | 41 | id_list.concat(Array(stream_id)) 42 | id_list.concat(Array(id)) 43 | id_list.concat(Array(ids)) 44 | 45 | id_part = nil 46 | if not id_list.empty? 47 | id_part = ID.compound_id(id_list) 48 | stream_name = "#{stream_name}#{id_separator}#{id_part}" 49 | end 50 | 51 | stream_name 52 | end 53 | 54 | def self.split(stream_name) 55 | stream_name.split(id_separator, 2) 56 | end 57 | 58 | def self.get_id(stream_name) 59 | split(stream_name)[1] 60 | end 61 | 62 | def self.get_ids(stream_name) 63 | ids = get_id(stream_name) 64 | 65 | return [] if ids.nil? 66 | 67 | ID.parse(ids) 68 | end 69 | 70 | def self.get_cardinal_id(stream_name) 71 | id = get_id(stream_name) 72 | 73 | return nil if id.nil? 74 | 75 | ID.get_cardinal_id(id) 76 | end 77 | 78 | def self.get_category(stream_name) 79 | split(stream_name)[0] 80 | end 81 | 82 | def self.category?(stream_name) 83 | !stream_name.include?(id_separator) 84 | end 85 | 86 | def self.get_category_type(stream_name) 87 | return nil unless stream_name.include?(category_type_separator) 88 | 89 | category = get_category(stream_name) 90 | 91 | category.split(category_type_separator)[1] 92 | end 93 | 94 | def self.get_type(*args) 95 | get_category_type(*args) 96 | end 97 | 98 | def self.get_category_types(stream_name) 99 | type_list = get_type(stream_name) 100 | 101 | return [] if type_list.nil? 102 | 103 | type_list.split(compound_type_separator) 104 | end 105 | 106 | def self.get_types(*args) 107 | get_category_types(*args) 108 | end 109 | 110 | def self.get_entity_name(stream_name) 111 | get_category(stream_name).split(category_type_separator)[0] 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/message_store/write.rb: -------------------------------------------------------------------------------- 1 | module MessageStore 2 | module Write 3 | def self.included(cls) 4 | cls.class_exec do 5 | include Dependency 6 | include TemplateMethod 7 | include Log::Dependency 8 | 9 | extend Build 10 | extend Call 11 | extend Configure 12 | 13 | dependency :identifier, Identifier::UUID::Random 14 | 15 | template_method! :configure 16 | template_method! :write 17 | end 18 | end 19 | 20 | module Build 21 | def build(session: nil) 22 | instance = new 23 | Identifier::UUID::Random.configure(instance) 24 | instance.configure(session: session) 25 | instance 26 | end 27 | end 28 | 29 | module Configure 30 | def configure(receiver, session: nil, attr_name: nil) 31 | attr_name ||= :write 32 | instance = build(session: session) 33 | receiver.public_send "#{attr_name}=", instance 34 | end 35 | end 36 | 37 | module Call 38 | def call(message_data, stream_name, expected_version: nil, session: nil) 39 | instance = build(session: session) 40 | instance.(message_data, stream_name, expected_version: expected_version) 41 | end 42 | end 43 | 44 | def call(message_data, stream_name, expected_version: nil) 45 | batch = Array(message_data) 46 | 47 | logger.trace(tag: :write) do 48 | message_types = batch.map {|message_data| message_data.type }.uniq.join(', ') 49 | "Writing message data (Types: #{message_types}, Stream Name: #{stream_name}, Expected Version: #{expected_version.inspect}, Number of Messages: #{batch.length})" 50 | end 51 | logger.trace(tags: [:data, :message_data]) { batch.pretty_inspect } 52 | 53 | set_ids(batch) 54 | 55 | position = write(batch, stream_name, expected_version: expected_version) 56 | 57 | logger.info(tag: :write) do 58 | message_types = batch.map {|message_data| message_data.type }.uniq.join(', ') 59 | "Wrote message data (Types: #{message_types}, Stream Name: #{stream_name}, Expected Version: #{expected_version.inspect}, Number of Messages: #{batch.length})" 60 | end 61 | logger.info(tags: [:data, :message_data]) { batch.pretty_inspect } 62 | 63 | position 64 | end 65 | alias :write :call 66 | 67 | def set_ids(batch) 68 | batch.each do |message_data| 69 | if message_data.id.nil? 70 | message_data.id = identifier.get 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /library-symlinks.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if [ -z ${LIBRARIES_HOME+x} ]; then 4 | echo "LIBRARIES_HOME must be set to the libraries directory path... exiting" 5 | exit 1 6 | fi 7 | 8 | if [ ! -d "$LIBRARIES_HOME" ]; then 9 | echo "$LIBRARIES_HOME does not exist... exiting" 10 | exit 1 11 | fi 12 | 13 | function make_directory { 14 | directory=$1 15 | 16 | lib_directory="$LIBRARIES_HOME/$directory" 17 | 18 | if [ ! -d "$lib_directory" ]; then 19 | echo "- making directory $lib_directory" 20 | mkdir -p "$lib_directory" 21 | fi 22 | } 23 | 24 | function remove_lib_symlinks { 25 | name=$1 26 | directory=$2 27 | 28 | dest="$LIBRARIES_HOME" 29 | if [ ! -z "$directory" ]; then 30 | dest="$dest/$directory" 31 | fi 32 | dest="$dest/$name" 33 | 34 | for entry in $dest*; do 35 | if [ -h "$entry" ]; then 36 | echo "- removing symlink: $entry" 37 | rm $entry 38 | fi 39 | done 40 | } 41 | 42 | function symlink_lib { 43 | name=$1 44 | directory=$2 45 | 46 | echo 47 | echo "Symlinking $name" 48 | echo "- - -" 49 | 50 | remove_lib_symlinks $name $directory 51 | 52 | src="$(pwd)/lib" 53 | dest="$LIBRARIES_HOME" 54 | if [ ! -z "$directory" ]; then 55 | src="$src/$directory" 56 | dest="$dest/$directory" 57 | 58 | make_directory $directory 59 | fi 60 | src="$src/$name" 61 | 62 | echo "- destination is $dest" 63 | 64 | full_name=$directory/$name 65 | 66 | for entry in $src*; do 67 | entry_basename=$(basename $entry) 68 | dest_item="$dest/$entry_basename" 69 | 70 | echo "- symlinking $entry_basename to $dest_item" 71 | 72 | cmd="ln -s $entry $dest_item" 73 | echo $cmd 74 | ($cmd) 75 | done 76 | 77 | echo "- - -" 78 | echo "($name done)" 79 | echo 80 | } 81 | -------------------------------------------------------------------------------- /load_path.rb: -------------------------------------------------------------------------------- 1 | bundler_standalone_loader = 'gems/bundler/setup' 2 | 3 | begin 4 | require_relative bundler_standalone_loader 5 | rescue LoadError 6 | warn "WARNING: Standalone bundle loader is not at #{bundler_standalone_loader}. Using Bundler to load gems." 7 | require "bundler/setup" 8 | Bundler.require 9 | end 10 | 11 | lib_dir = File.expand_path('lib', __dir__) 12 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 13 | 14 | libraries_dir = ENV['LIBRARIES_HOME'] 15 | unless libraries_dir.nil? 16 | libraries_dir = File.expand_path(libraries_dir) 17 | $LOAD_PATH.unshift libraries_dir unless $LOAD_PATH.include?(libraries_dir) 18 | end 19 | -------------------------------------------------------------------------------- /message_store.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | Gem::Specification.new do |s| 3 | s.name = 'evt-message_store' 4 | s.version = '2.4.0.1' 5 | s.summary = 'Common primitives for platform-specific message store implementations' 6 | s.description = ' ' 7 | 8 | s.authors = ['The Eventide Project'] 9 | s.email = 'opensource@eventide-project.org' 10 | s.homepage = 'https://github.com/eventide-project/message-store' 11 | s.licenses = ['MIT'] 12 | 13 | s.require_paths = ['lib'] 14 | s.files = Dir.glob('{lib}/**/*') 15 | s.platform = Gem::Platform::RUBY 16 | s.required_ruby_version = '>= 2.4.0' 17 | 18 | s.add_runtime_dependency 'evt-casing' 19 | s.add_runtime_dependency 'evt-schema' 20 | s.add_runtime_dependency 'evt-initializer' 21 | s.add_runtime_dependency 'evt-identifier-uuid' 22 | s.add_runtime_dependency 'evt-transform' 23 | s.add_runtime_dependency 'evt-template_method' 24 | s.add_runtime_dependency 'evt-async_invocation' 25 | 26 | s.add_development_dependency 'test_bench' 27 | end 28 | -------------------------------------------------------------------------------- /remove-lib-symlinks.sh: -------------------------------------------------------------------------------- 1 | source ./library-symlinks.sh 2 | 3 | remove_lib_symlinks 'message_store' 4 | -------------------------------------------------------------------------------- /symlink-lib.sh: -------------------------------------------------------------------------------- 1 | source ./library-symlinks.sh 2 | 3 | symlink_lib 'message_store' 4 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test/automated' 2 | -------------------------------------------------------------------------------- /test/automated.rb: -------------------------------------------------------------------------------- 1 | require_relative './test_init' 2 | 3 | TestBench::Run.( 4 | 'test/automated', 5 | exclude: '{_*,*sketch*,*_init,*_tests}.rb' 6 | ) or exit(false) 7 | -------------------------------------------------------------------------------- /test/automated/automated_init.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_init' 2 | -------------------------------------------------------------------------------- /test/automated/get_last/call.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Get Last" do 4 | context "Call" do 5 | stream_name = Controls::StreamName.example 6 | type = Controls::MessageData.type 7 | 8 | context "Not Specialized" do 9 | cls = Class.new do 10 | include MessageStore::Get::Stream::Last 11 | end 12 | 13 | test "Raises template method error" do 14 | assert_raises(TemplateMethod::Error) do 15 | cls.(stream_name) 16 | end 17 | end 18 | end 19 | 20 | context "Specialized" do 21 | specialized_method_executed = false 22 | 23 | cls = Class.new do 24 | include MessageStore::Get::Stream::Last 25 | 26 | define_method(:call) do |_stream_name, type=nil| 27 | specialized_method_executed = true if _stream_name == stream_name 28 | end 29 | end 30 | 31 | cls.(stream_name, type) 32 | 33 | test "Executes specialized method" do 34 | assert(specialized_method_executed) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/automated/get_last/configure.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Get Last" do 4 | context "Configure" do 5 | context "Session Given" do 6 | session = Object.new 7 | 8 | receiver = OpenStruct.new 9 | 10 | Controls::GetLast::Example.configure(receiver, session: session) 11 | 12 | get_last = receiver.get_last 13 | 14 | test "Session is set" do 15 | assert(get_last.session == session) 16 | end 17 | end 18 | 19 | context "Session Not Given" do 20 | receiver = OpenStruct.new 21 | 22 | Controls::GetLast::Example.configure(receiver) 23 | 24 | get_last = receiver.get_last 25 | 26 | test "Default session is used" do 27 | assert(get_last.session == Controls::GetLast.default_session) 28 | end 29 | end 30 | 31 | context "Attribute Name Specified" do 32 | receiver = OpenStruct.new 33 | 34 | Controls::GetLast::Example.configure(receiver, attr_name: :some_attr) 35 | 36 | get_last = receiver.some_attr 37 | 38 | test do 39 | assert(get_last.is_a?(Get::Stream::Last)) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/automated/get_last/substitute.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Get Last" do 4 | context "Substitute" do 5 | stream_name = Controls::StreamName.example 6 | 7 | control_message_data = Controls::MessageData::Read.example 8 | 9 | context "Message Is Set" do 10 | substitute = SubstAttr::Substitute.build(Controls::GetLast::Example) 11 | 12 | substitute.set(stream_name, control_message_data) 13 | 14 | message_data = substitute.(stream_name) 15 | 16 | test "Returns the message that was set" do 17 | assert(message_data == control_message_data) 18 | end 19 | end 20 | 21 | context "Message Not Set" do 22 | substitute = SubstAttr::Substitute.build(Controls::GetLast::Example) 23 | 24 | message_data = substitute.(stream_name) 25 | 26 | test "Returns nil" do 27 | assert(message_data.nil?) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/automated/id/compose.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "ID" do 4 | context "Compose" do 5 | context "Singular ID" do 6 | context "Not Nil" do 7 | id = 'some_id' 8 | 9 | compound_id = MessageStore::ID.id(id) 10 | 11 | test "Composed ID is the singular ID" do 12 | assert(compound_id == id) 13 | end 14 | end 15 | 16 | context "Nil" do 17 | id = nil 18 | 19 | test "Is an error" do 20 | assert_raises(MessageStore::ID::Error) do 21 | MessageStore::ID.id(id) 22 | end 23 | end 24 | end 25 | end 26 | 27 | context "List of IDs" do 28 | context "Not Empty" do 29 | id = ['some_id', 'some_other_id'] 30 | 31 | compound_id = MessageStore::ID.id(id) 32 | 33 | test "Composed ID is the concatenation of the list of IDs delimited by the '+'' sign" do 34 | assert(compound_id == 'some_id+some_other_id') 35 | end 36 | end 37 | 38 | context "Empty" do 39 | id = [] 40 | 41 | test "Is an error" do 42 | assert_raises(MessageStore::ID::Error) do 43 | MessageStore::ID.id(id) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/automated/id/get_cardinal_id.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Get Cardinal ID" do 4 | context "Singular ID" do 5 | id = 'some_id' 6 | 7 | cardinal_id = ID.get_cardinal_id(id) 8 | 9 | test "Cardinal ID is the ID" do 10 | assert(cardinal_id == 'some_id') 11 | end 12 | end 13 | 14 | context "Compound ID" do 15 | id = 'some_id+some_other_id' 16 | 17 | cardinal_id = ID.get_cardinal_id(id) 18 | 19 | test "ID is the first ID" do 20 | assert(cardinal_id == 'some_id') 21 | end 22 | end 23 | 24 | context "Nil" do 25 | id = nil 26 | 27 | test "Is an error" do 28 | assert_raises(MessageStore::ID::Error) do 29 | ID.get_cardinal_id(id) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/automated/id/parse.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Parse" do 4 | context "Singular ID" do 5 | id = 'some_id' 6 | 7 | parsed_id = ID.parse(id) 8 | 9 | test "Parsed IDis a list containing the ID" do 10 | assert(parsed_id == ['some_id']) 11 | end 12 | end 13 | 14 | context "Compound ID" do 15 | id = 'some_id+some_other_id' 16 | 17 | parsed_id = ID.parse(id) 18 | 19 | test "Parsed IDis a list containing the individual IDs" do 20 | assert(parsed_id == ['some_id', 'some_other_id']) 21 | end 22 | end 23 | 24 | context "Nil" do 25 | id = nil 26 | 27 | test "Is an error" do 28 | assert_raises(MessageStore::ID::Error) do 29 | ID.get_cardinal_id(id) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/automated/iterator/batch_depletion/depleted/batch_index_is_batch_length.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Batch Depletion" do 5 | context "Depleted" do 6 | context "Batch Index is the Batch Length" do 7 | iterator = Read::Iterator.build 8 | 9 | iterator.batch = [1, 11] 10 | iterator.batch_index = 2 11 | 12 | batch_depleted = iterator.batch_depleted? 13 | 14 | test "Batch is depleted" do 15 | assert(batch_depleted) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/automated/iterator/batch_depletion/depleted/batch_is_empty.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Batch Depletion" do 5 | context "Depleted" do 6 | context "Batch in Empty" do 7 | iterator = Read::Iterator.build 8 | 9 | iterator.batch = [] 10 | 11 | batch_depleted = iterator.batch_depleted? 12 | 13 | test "Batch is depleted" do 14 | assert(batch_depleted) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/automated/iterator/batch_depletion/depleted/batch_is_nil.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Batch Depletion" do 5 | context "Depleted" do 6 | context "Batch in Nil" do 7 | iterator = Read::Iterator.build 8 | 9 | iterator.batch = nil 10 | 11 | batch_depleted = iterator.batch_depleted? 12 | 13 | test "Batch is depleted" do 14 | assert(batch_depleted) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/automated/iterator/batch_depletion/not_depleted/batch_index_less_than_batch_length.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Batch Depletion" do 5 | context "Not Depleted" do 6 | context "Batch Index is Less Than the Batch Length" do 7 | iterator = Read::Iterator.build 8 | 9 | iterator.batch = [1, 11] 10 | iterator.batch_index = 1 11 | 12 | batch_depleted = iterator.batch_depleted? 13 | 14 | test "Batch is not depleted" do 15 | refute(batch_depleted) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/automated/iterator/batch_initialization.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Iterator" do 4 | context "Batch Initialization" do 5 | context "Batch is Nil" do 6 | iterator = Read::Iterator.build 7 | iterator.batch = nil 8 | 9 | test "Batch is not initialized" do 10 | refute(iterator.batch_initialized?) 11 | end 12 | end 13 | 14 | context "Batch is Not Nil" do 15 | iterator = Read::Iterator.build 16 | iterator.batch = :something 17 | 18 | test "Batch is initialized" do 19 | assert(iterator.batch_initialized?) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/automated/iterator/next/category/next.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Next" do 5 | count = 3 6 | get = Controls::Get.example(batch_size: 2, count: count) 7 | 8 | iterator = Read::Iterator.build 9 | iterator.get = get 10 | 11 | batch = [] 12 | 13 | count.times do 14 | message_data = iterator.next 15 | batch << message_data unless message_data.nil? 16 | end 17 | 18 | test "Gets each message" do 19 | assert(batch.length == count) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/next/category/next_from_position.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Next" do 5 | count = 3 6 | 7 | category = Controls::Category.example 8 | get = Controls::Get.example(stream_name: category, batch_size: 2, count: count, global_position_offset: -> (x) { 2 + x ** 2 }) 9 | 10 | iterator = Read::Iterator.build(3) 11 | iterator.get = get 12 | 13 | batch = [] 14 | 15 | count.times do 16 | message_data = iterator.next 17 | 18 | break if message_data.nil? 19 | batch << message_data 20 | end 21 | 22 | test "Gets each message" do 23 | assert(batch.length == count - 1) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/automated/iterator/next/stream/next.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Next" do 5 | count = 3 6 | get = Controls::Get.example(batch_size: 2, count: count) 7 | 8 | iterator = Read::Iterator.build 9 | iterator.get = get 10 | 11 | batch = [] 12 | 13 | count.times do 14 | message_data = iterator.next 15 | batch << message_data unless message_data.nil? 16 | end 17 | 18 | test "Gets each message" do 19 | assert(batch.length == count) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/next/stream/next_from_position.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Next" do 5 | count = 3 6 | get = Controls::Get.example(batch_size: 2, count: count) 7 | 8 | iterator = Read::Iterator.build(1) 9 | iterator.get = get 10 | 11 | batch = [] 12 | 13 | count.times do 14 | message_data = iterator.next 15 | batch << message_data unless message_data.nil? 16 | end 17 | 18 | test "Gets each message" do 19 | assert(batch.length == count - 1) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/no_further_message_data.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Iterator" do 4 | context "Next" do 5 | context "No further message data" do 6 | count = 3 7 | get = Controls::Get.example(batch_size: 2, count: count) 8 | 9 | iterator = Read::Iterator.build 10 | iterator.get = get 11 | 12 | count.times { iterator.next } 13 | 14 | last = iterator.next 15 | 16 | test "Results in nil" do 17 | assert(last.nil?) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/automated/iterator/stream_depletion/depleted/resulting_batch_size_less_than_requested_batch_size.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Stream Depletion" do 5 | context "Depleted" do 6 | context "Resulting Batch Size is Less Than the Requested Batch Size" do 7 | get = Controls::Get.example(batch_size: 2) 8 | 9 | iterator = Read::Iterator.build 10 | iterator.get = get 11 | 12 | iterator.batch = [1] 13 | 14 | stream_depleted = iterator.stream_depleted? 15 | 16 | test "Stream is depleted" do 17 | assert(stream_depleted) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/stream_depletion/not_depleted/batch_is_uninitialized.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Stream Depletion" do 5 | context "Not Depleted" do 6 | context "Batch is Uninitialized" do 7 | get = Controls::Get.example(batch_size: 2) 8 | 9 | iterator = Read::Iterator.build 10 | iterator.get = get 11 | 12 | iterator.batch = nil 13 | 14 | stream_depleted = iterator.stream_depleted? 15 | 16 | test "Stream is not depleted" do 17 | refute(stream_depleted) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/stream_depletion/not_depleted/resulting_batch_size_equal_to_requested_batch_size.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Stream Depletion" do 5 | context "Not Depleted" do 6 | context "Resulting Batch Size is Equal to the Requested Batch Size" do 7 | get = Controls::Get.example(batch_size: 2) 8 | 9 | iterator = Read::Iterator.build 10 | iterator.get = get 11 | 12 | iterator.batch = [1, 11] 13 | 14 | stream_depleted = iterator.stream_depleted? 15 | 16 | test "Stream is not depleted" do 17 | refute(stream_depleted) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator/stream_depletion/not_depleted/resulting_batch_size_greater_than_requested_batch_size.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../automated_init' 2 | 3 | context "Iterator" do 4 | context "Stream Depletion" do 5 | context "Not Depleted" do 6 | context "Resulting Batch Size is Greater Than the Requested Batch Size" do 7 | get = Controls::Get.example(batch_size: 2) 8 | 9 | iterator = Read::Iterator.build 10 | iterator.get = get 11 | 12 | iterator.batch = [1, 11, 111] 13 | 14 | stream_depleted = iterator.stream_depleted? 15 | 16 | test "Stream is not depleted" do 17 | refute(stream_depleted) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/automated/iterator_tests.rb: -------------------------------------------------------------------------------- 1 | require_relative './automated_init' 2 | 3 | TestBench::Run.( 4 | 'test/automated/iterator', 5 | exclude: '{_*,*sketch*,*_init,*_tests}.rb' 6 | ) or exit(false) 7 | -------------------------------------------------------------------------------- /test/automated/message_data/case_equality.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "MessageData" do 4 | context "Case Equality" do 5 | read_data = Controls::MessageData::Read.example 6 | 7 | context "Type is equal" do 8 | equal = read_data === read_data.type 9 | 10 | context "MessageData matches" do 11 | assert(equal) 12 | end 13 | end 14 | 15 | context "Type is not equal" do 16 | equal = read_data === SecureRandom.hex 17 | 18 | context "MessageData does not match" do 19 | refute(equal) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/automated/message_data/hash.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "MessageData" do 4 | context "Hash" do 5 | context "JSON serialize" do 6 | example_hash = Controls::MessageData::Hash.example 7 | control_serialized_text = Controls::MessageData::Hash::JSON.text 8 | 9 | serialized_text = Transform::Write.(example_hash, :json) 10 | 11 | assert(serialized_text == control_serialized_text) 12 | end 13 | 14 | context "JSON deserialize" do 15 | example_serialized_text = Controls::MessageData::Hash::JSON.text 16 | control_deserialized_hash = Controls::MessageData::Hash.example 17 | 18 | deserialized_hash = Transform::Read.(example_serialized_text, :json, MessageData::Hash) 19 | 20 | assert(deserialized_hash == control_deserialized_hash) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/automated/read/asynchronous_result.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Read" do 4 | context "Asynchronous Result" do 5 | reader = Controls::Read.example 6 | 7 | res = reader.() { } 8 | 9 | test "Returns a result that fails if actuated" do 10 | assert(res == AsyncInvocation::Incorrect) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/automated/read/missing_block_error.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Read" do 4 | context "No block supplied" do 5 | reader = Controls::Read.example 6 | 7 | test "Is incorrect" do 8 | assert_raises(Read::Error) do 9 | reader.() 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/automated/stream_name/category_predicate.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Category Predicate" do 5 | context "Stream Name Contains a Dash (-)" do 6 | stream_name = 'someStream-some_id' 7 | 8 | is_category = StreamName.category?(stream_name) 9 | 10 | test "Not a category" do 11 | refute(is_category) 12 | end 13 | end 14 | 15 | context "Stream Name Contains no Dash (-)" do 16 | stream_name = 'someStream' 17 | 18 | is_category = StreamName.category?(stream_name) 19 | 20 | test "Is a category" do 21 | assert(is_category) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/cardinal_id.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Cardinal ID" do 6 | context "Cardinal ID" do 7 | stream_name = StreamName.stream_name('someCategory', cardinal_id: 'some_cardinal_id') 8 | 9 | test "Stream name is the category and the cardinal ID" do 10 | assert(stream_name == 'someCategory-some_cardinal_id') 11 | end 12 | end 13 | 14 | context "Cardinal ID and Positional ID" do 15 | stream_name = StreamName.stream_name('someCategory', 'some_positional_id', cardinal_id: 'some_cardinal_id') 16 | 17 | test "Stream name is the category, the cardinal ID, and the positional ID" do 18 | assert(stream_name == 'someCategory-some_cardinal_id+some_positional_id') 19 | end 20 | end 21 | 22 | context "Cardinal ID and Named ID Argument" do 23 | stream_name = StreamName.stream_name('someCategory', cardinal_id: 'some_cardinal_id', id: 'some_named_id') 24 | 25 | test "Stream name is the category, the cardinal ID, and the named ID" do 26 | assert(stream_name == 'someCategory-some_cardinal_id+some_named_id') 27 | end 28 | end 29 | 30 | context "Cardinal ID and Named IDs Argument" do 31 | stream_name = StreamName.stream_name('someCategory', cardinal_id: 'some_cardinal_id', ids: 'some_named_ids') 32 | 33 | test "Stream name is the category, the cardinal ID, and the named IDs" do 34 | assert(stream_name == 'someCategory-some_cardinal_id+some_named_ids') 35 | end 36 | end 37 | 38 | context "Cardinal ID, Named ID, and Named IDs Argument" do 39 | stream_name = StreamName.stream_name('someCategory', cardinal_id: 'some_cardinal_id', id: 'some_named_id', ids: 'some_named_ids') 40 | 41 | test "Stream name is the category, the cardinal ID, the named ID, and the named IDs" do 42 | assert(stream_name == 'someCategory-some_cardinal_id+some_named_id+some_named_ids') 43 | end 44 | end 45 | 46 | context "Cardinal ID, Positional ID, Named ID, and Named IDs Argument" do 47 | stream_name = StreamName.stream_name('someCategory', 'some_positional_id', cardinal_id: 'some_cardinal_id', id: 'some_named_id', ids: 'some_named_ids') 48 | 49 | test "Stream name is the category, the cardinal ID, the positional ID, the named ID, and the named IDs" do 50 | assert(stream_name == 'someCategory-some_cardinal_id+some_positional_id+some_named_id+some_named_ids') 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/category.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Category" do 6 | stream_name = StreamName.stream_name('someCategory') 7 | 8 | test "Stream name is the category" do 9 | assert(stream_name == 'someCategory') 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/compound_id.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Compound ID" do 6 | context "Positional ID Argument" do 7 | stream_name = StreamName.stream_name('someCategory', ['some_positional_id', 'some_other_positional_id']) 8 | 9 | test "Stream name is the category and the ID" do 10 | assert(stream_name == 'someCategory-some_positional_id+some_other_positional_id') 11 | end 12 | end 13 | 14 | context "Named ID Argument" do 15 | stream_name = StreamName.stream_name('someCategory', id: ['some_named_id', 'some_other_named_id']) 16 | 17 | test "Stream name is the category, and the named ID" do 18 | assert(stream_name == 'someCategory-some_named_id+some_other_named_id') 19 | end 20 | end 21 | 22 | context "Named IDs Argument" do 23 | stream_name = StreamName.stream_name('someCategory', ids: ['some_named_id', 'some_other_named_id']) 24 | 25 | test "Stream name is the category, and the named IDs" do 26 | assert(stream_name == 'someCategory-some_named_id+some_other_named_id') 27 | end 28 | end 29 | 30 | context "Named ID Argument, and Plural Named IDs Argument" do 31 | stream_name = StreamName.stream_name('someCategory', id: ['some_named_id', 'some_other_named_id'], ids: ['yet_another_named_id', 'and_another_named_id']) 32 | 33 | test "Stream name is the category, the named ID, and the named IDs" do 34 | assert(stream_name == 'someCategory-some_named_id+some_other_named_id+yet_another_named_id+and_another_named_id') 35 | end 36 | end 37 | 38 | context "Positional ID Argument, Named ID Argument, and Named IDs Argument" do 39 | stream_name = StreamName.stream_name('someCategory', ['some_positional_id', 'some_other_positional_id'], id: ['some_named_id', 'some_other_named_id'], ids: ['yet_another_named_id', 'and_another_named_id']) 40 | 41 | test "Stream name is the category, the positional ID, and the named ID, and the named IDs" do 42 | assert(stream_name == 'someCategory-some_positional_id+some_other_positional_id+some_named_id+some_other_named_id+yet_another_named_id+and_another_named_id') 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/missing_category_error.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Missing Category" do 6 | test "Is an error" do 7 | assert_raises(MessageStore::StreamName::Error) do 8 | StreamName.stream_name(nil) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/singular_ids.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Singular IDs" do 6 | context "Positional ID" do 7 | stream_name = StreamName.stream_name('someCategory', 'some_positional_id') 8 | 9 | test "Stream name is the category and the positional ID" do 10 | assert(stream_name == 'someCategory-some_positional_id') 11 | end 12 | end 13 | 14 | context "Named ID Argument" do 15 | stream_name = StreamName.stream_name('someCategory', id: 'some_named_id') 16 | 17 | test "Stream name is the category and the named ID" do 18 | assert(stream_name == 'someCategory-some_named_id') 19 | end 20 | end 21 | 22 | context "Named IDs Argument" do 23 | stream_name = StreamName.stream_name('someCategory', ids: 'some_named_ids') 24 | 25 | test "Stream name is the category and the named IDs" do 26 | assert(stream_name == 'someCategory-some_named_ids') 27 | end 28 | end 29 | 30 | context "Named ID and Named IDs Argument" do 31 | stream_name = StreamName.stream_name('someCategory', id: 'some_named_id', ids: 'some_named_ids') 32 | 33 | test "Stream name is the category, the cardinal ID, and the named ID" do 34 | assert(stream_name == 'someCategory-some_named_id+some_named_ids') 35 | end 36 | end 37 | 38 | context "Positional ID, Named ID, and Named IDs Argument" do 39 | stream_name = StreamName.stream_name('someCategory', 'some_positional_id', id: 'some_named_id', ids: 'some_named_ids') 40 | 41 | test "Stream name is the category, the positional ID, the named ID, and the named IDs" do 42 | assert(stream_name == 'someCategory-some_positional_id+some_named_id+some_named_ids') 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/stream_name.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Stream Name" do 6 | context "Category, Type, Types, Positional ID, Cardinal ID, Named ID, and Named IDs" do 7 | stream_name = StreamName.stream_name('someCategory', 'some_id', cardinal_id: 'some_cardinal_id', id: 'some_named_id', ids: ['some_other_named_id', 'yet_another_named_id'], type: 'someType', types: ['someOtherType', 'yetAnotherType']) 8 | 9 | test "Stream name is the category, type, types, cardinal ID, positional ID, named ID, and named IDs" do 10 | assert(stream_name == 'someCategory:someType+someOtherType+yetAnotherType-some_cardinal_id+some_id+some_named_id+some_other_named_id+yet_another_named_id') 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/automated/stream_name/compose/types.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Compose" do 5 | context "Types" do 6 | context "Singular Types" do 7 | context "Category and Type" do 8 | stream_name = StreamName.stream_name('someCategory', type: 'someType') 9 | 10 | test "Stream name is the category and the Type" do 11 | assert(stream_name == 'someCategory:someType') 12 | end 13 | end 14 | 15 | context "Category and Types" do 16 | stream_name = StreamName.stream_name('someCategory', types: 'someTypes') 17 | 18 | test "Stream name is the category and the types" do 19 | assert(stream_name == 'someCategory:someTypes') 20 | end 21 | end 22 | 23 | context "Category, Type, and Types" do 24 | stream_name = StreamName.stream_name('someCategory', type: 'someType', types: 'someTypes') 25 | 26 | test "Stream name is the category and the types" do 27 | assert(stream_name == 'someCategory:someType+someTypes') 28 | end 29 | end 30 | end 31 | 32 | context "Plural Types" do 33 | context "Category and Type" do 34 | stream_name = StreamName.stream_name('someCategory', type: ['someType', 'someOtherType']) 35 | 36 | test "Stream name is the category and the type" do 37 | assert(stream_name == 'someCategory:someType+someOtherType') 38 | end 39 | end 40 | 41 | context "Category and Types" do 42 | stream_name = StreamName.stream_name('someCategory', types: ['someType', 'someOtherType']) 43 | 44 | test "Stream name is the category and the types" do 45 | assert(stream_name == 'someCategory:someType+someOtherType') 46 | end 47 | end 48 | 49 | context "Category, Type, and Types" do 50 | stream_name = StreamName.stream_name('someCategory', type: ['someType', 'someOtherType'], types: ['yetAnotherType', 'andAnotherType']) 51 | 52 | test "Stream name is the category and the types delimited by the plus (+) sign" do 53 | assert(stream_name == 'someCategory:someType+someOtherType+yetAnotherType+andAnotherType') 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_cardinal_id.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get Cardinal ID" do 5 | context "Compound ID in the Stream Name" do 6 | id = 'some_id+some_other_id' 7 | stream_name = "someCategory-#{id}" 8 | 9 | cardinal_id = StreamName.get_cardinal_id(stream_name) 10 | 11 | test "ID is a list of values" do 12 | assert(cardinal_id == 'some_id') 13 | end 14 | end 15 | 16 | context "Single ID in the Stream Name" do 17 | id = 'some_id' 18 | stream_name = "someCategory-#{id}" 19 | 20 | cardinal_id = StreamName.get_cardinal_id(stream_name) 21 | 22 | test "ID list has a single entry" do 23 | assert(cardinal_id == 'some_id') 24 | end 25 | end 26 | 27 | context "No ID in the Stream Name" do 28 | stream_name = 'someStream' 29 | 30 | cardinal_id = StreamName.get_cardinal_id(stream_name) 31 | 32 | test "ID is an empty list" do 33 | assert(cardinal_id.nil?) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_category.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get Category" do 5 | category = 'someStream' 6 | 7 | context "Stream Name Contains an ID" do 8 | id = Identifier::UUID.random 9 | stream_name = "#{category}-#{id}" 10 | 11 | stream_category = StreamName.get_category(stream_name) 12 | 13 | test "Category name is the part of the stream name before the first dash" do 14 | assert(stream_category == category) 15 | end 16 | end 17 | 18 | context "Stream Name Contains no ID" do 19 | stream_name = category 20 | stream_category = StreamName.get_category(stream_name) 21 | 22 | test "Category name is the stream name" do 23 | assert(stream_category == category) 24 | end 25 | end 26 | 27 | context "Stream Name Contains Type" do 28 | stream_name = "#{category}:someType" 29 | stream_category = StreamName.get_category(stream_name) 30 | 31 | test "Category name is the stream name" do 32 | assert(stream_category == stream_name) 33 | end 34 | end 35 | 36 | context "Stream Name Contains Types" do 37 | stream_name = "#{category}:someType+someOtherType" 38 | stream_category = StreamName.get_category(stream_name) 39 | 40 | test "Category name is the stream name" do 41 | assert(stream_category == stream_name) 42 | end 43 | end 44 | 45 | context "Stream Name Contains ID and Types" do 46 | id = Identifier::UUID.random 47 | category_and_types = "#{category}:someType+someOtherType" 48 | stream_name = "#{category_and_types}-#{id}" 49 | 50 | stream_category = StreamName.get_category(stream_name) 51 | 52 | test "Category name is the stream name" do 53 | assert(stream_category == category_and_types) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_category_type.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get Type List" do 5 | context "Many Types" do 6 | stream_name = "someStream:someType+someOtherType" 7 | 8 | type_list = StreamName.get_type(stream_name) 9 | 10 | test "Types are the list of elements following a colon separator" do 11 | assert(type_list == 'someType+someOtherType') 12 | end 13 | end 14 | 15 | context "ID and Many Types" do 16 | stream_name = "someStream:someType+someOtherType-someID" 17 | 18 | type_list = StreamName.get_type(stream_name) 19 | 20 | test "Types are the list of elements following a colon separator and preceding the ID" do 21 | assert(type_list == 'someType+someOtherType') 22 | end 23 | end 24 | 25 | context "Single Type" do 26 | stream_name = "someStream:someType" 27 | 28 | type_list = StreamName.get_type(stream_name) 29 | 30 | test "Types are the list of elements following a colon separator and preceding the ID" do 31 | assert(type_list == 'someType') 32 | end 33 | end 34 | 35 | context "No type list in the stream name" do 36 | stream_name = "someStream" 37 | 38 | type_list = StreamName.get_type(stream_name) 39 | 40 | test "No types" do 41 | assert(type_list.nil?) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_category_types.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get Types" do 5 | context "Many Types" do 6 | test "Types are the list of elements following a colon separator" do 7 | stream_name = "someStream:someType+someOtherType" 8 | 9 | types = StreamName.get_types(stream_name) 10 | 11 | assert(types == ['someType', 'someOtherType']) 12 | end 13 | end 14 | 15 | context "ID and Many Types" do 16 | test "Types are the list of elements following a colon separator and preceding the ID" do 17 | stream_name = "someStream:someType+someOtherType-someID" 18 | 19 | types = StreamName.get_types(stream_name) 20 | 21 | assert(types == ['someType', 'someOtherType']) 22 | end 23 | end 24 | 25 | context "Single Type" do 26 | test "Types are the list of elements following a colon separator and preceding the ID" do 27 | stream_name = "someStream:someType" 28 | 29 | types = StreamName.get_types(stream_name) 30 | 31 | assert(types == ['someType']) 32 | end 33 | end 34 | 35 | context "No type list in the stream name" do 36 | types = StreamName.get_types('someStream') 37 | 38 | test "No types" do 39 | assert(types.empty?) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_entity_name.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get Entity Name" do 5 | category = 'someStream' 6 | 7 | context "Stream Name Contains an ID" do 8 | id = Identifier::UUID.random 9 | stream_name = "#{category}-#{id}" 10 | 11 | entity_name = StreamName.get_entity_name(stream_name) 12 | 13 | test "Entity name is the part of the stream name before the first dash" do 14 | assert(entity_name == category) 15 | end 16 | end 17 | 18 | context "Stream Name Contains no ID" do 19 | stream_name = category 20 | entity_name = StreamName.get_entity_name(stream_name) 21 | 22 | test "Entity name is the stream name" do 23 | assert(entity_name == category) 24 | end 25 | end 26 | 27 | context "Stream Name Contains Type" do 28 | stream_name = "#{category}:someType" 29 | entity_name = StreamName.get_entity_name(stream_name) 30 | 31 | test "Entity name is the category without any types" do 32 | assert(entity_name == category) 33 | end 34 | end 35 | 36 | context "Stream Name Contains Types" do 37 | stream_name = "#{category}:someType+someOtherType" 38 | entity_name = StreamName.get_entity_name(stream_name) 39 | 40 | test "Entity name is the category without any types" do 41 | assert(entity_name == category) 42 | end 43 | end 44 | 45 | context "Stream Name Contains ID and Types" do 46 | id = Identifier::UUID.random 47 | stream_name = "#{category}:someType+someOtherType-#{id}" 48 | entity_name = StreamName.get_entity_name(stream_name) 49 | 50 | test "Entity name is the category without any types" do 51 | assert(entity_name == category) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_id.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get ID" do 5 | context "Single ID in the Stream Name" do 6 | id = 'some_id' 7 | stream_name = "someCategory-#{id}" 8 | 9 | stream_id = StreamName.get_id(stream_name) 10 | 11 | test "ID value is parsed" do 12 | assert(stream_id == 'some_id') 13 | end 14 | end 15 | 16 | context "No ID in the Stream Name" do 17 | stream_id = StreamName.get_id('someStream') 18 | 19 | test "ID is nil" do 20 | assert(stream_id.nil?) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/automated/stream_name/parse/get_ids.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../automated_init' 2 | 3 | context "Stream Name" do 4 | context "Get IDs" do 5 | context "Compound ID in the Stream Name" do 6 | id = 'some_id+some_other_id' 7 | stream_name = "someCategory-#{id}" 8 | 9 | stream_ids = StreamName.get_ids(stream_name) 10 | 11 | test "ID is a list of values" do 12 | assert(stream_ids == ['some_id', 'some_other_id']) 13 | end 14 | end 15 | 16 | context "Single ID in the Stream Name" do 17 | id = 'some_id' 18 | stream_name = "someCategory-#{id}" 19 | 20 | stream_ids = StreamName.get_ids(stream_name) 21 | 22 | test "ID list has a single entry" do 23 | assert(stream_ids == ['some_id']) 24 | end 25 | end 26 | 27 | context "No ID in the Stream Name" do 28 | stream_name = 'someStream' 29 | 30 | stream_ids = StreamName.get_ids(stream_name) 31 | 32 | test "ID is an empty list" do 33 | assert(stream_ids == []) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/automated/write/ids_are_assigned.rb: -------------------------------------------------------------------------------- 1 | require_relative '../automated_init' 2 | 3 | context "Write" do 4 | stream_name = Controls::StreamName.example 5 | 6 | write_message_1 = Controls::MessageData::Write.example(id: :none) 7 | write_message_2 = Controls::MessageData::Write.example 8 | write_message_3 = Controls::MessageData::Write.example(id: :none) 9 | 10 | id = write_message_2.id 11 | 12 | batch = [write_message_1, write_message_2, write_message_3] 13 | 14 | Controls::Write::Example.(batch, stream_name) 15 | 16 | context "Missing IDs Are Assigned" do 17 | test "Message 1" do 18 | refute(write_message_1.id.nil?) 19 | end 20 | 21 | test "Message 3" do 22 | refute(write_message_3.id.nil?) 23 | end 24 | end 25 | 26 | context "Existing IDs Are not Assigned" do 27 | test "Message 2" do 28 | assert(write_message_2.id == id) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_init.rb: -------------------------------------------------------------------------------- 1 | ENV['CONSOLE_DEVICE'] ||= 'stdout' 2 | ENV['LOG_COLOR'] ||= 'on' 3 | 4 | if ENV['LOG_LEVEL'] 5 | ENV['LOGGER'] ||= 'on' 6 | else 7 | ENV['LOG_LEVEL'] ||= 'trace' 8 | end 9 | 10 | ENV['LOGGER'] ||= 'off' 11 | ENV['LOG_OPTIONAL'] ||= 'on' 12 | 13 | puts RUBY_DESCRIPTION 14 | 15 | require_relative '../init.rb' 16 | 17 | require 'test_bench'; TestBench.activate 18 | 19 | require 'message_store/controls' 20 | 21 | include MessageStore 22 | --------------------------------------------------------------------------------