pax_global_header00006660000000000000000000000064142024632160014512gustar00rootroot0000000000000052 comment=c939eee4f517866f92cc584a1c6c8cdb39d742a0 morpher-0.4.2/000077500000000000000000000000001420246321600131715ustar00rootroot00000000000000morpher-0.4.2/.circle.yml000066400000000000000000000001151420246321600152300ustar00rootroot00000000000000--- ruby: version: 1.9.3-p429 test: override: - bundle exec rake ci morpher-0.4.2/.github/000077500000000000000000000000001420246321600145315ustar00rootroot00000000000000morpher-0.4.2/.github/dependabot.yml000066400000000000000000000001711420246321600173600ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: bundler vendor: true directory: / schedule: interval: daily morpher-0.4.2/.github/workflows/000077500000000000000000000000001420246321600165665ustar00rootroot00000000000000morpher-0.4.2/.github/workflows/ci.yml000066400000000000000000000027651420246321600177160ustar00rootroot00000000000000name: CI on: pull_request jobs: base: name: Base steps runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Check Whitespace run: git diff --check -- HEAD~1 ruby-spec: name: Unit Specs runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: fail-fast: false matrix: ruby: [ruby-2.6, ruby-2.7, ruby-3.0] os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rspec spec/unit ruby-mutant: name: Mutation coverage runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: ruby: [ruby-2.6, ruby-2.7, ruby-3.0] os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec mutant run ruby-rubocop: name: Rubocop runs-on: ${{ matrix.os }} timeout-minutes: 5 strategy: fail-fast: false matrix: ruby: [ruby-2.6, ruby-2.7, ruby-3.0] os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rubocop morpher-0.4.2/.rspec000066400000000000000000000000701420246321600143030ustar00rootroot00000000000000--color --warnings --order random --require spec_helper morpher-0.4.2/.rubocop.yml000066400000000000000000000006501420246321600154440ustar00rootroot00000000000000AllCops: NewCops: enable Layout/ExtraSpacing: Enabled: false Layout/HashAlignment: EnforcedHashRocketStyle: table EnforcedColonStyle: table Layout/AccessModifierIndentation: EnforcedStyle: outdent Style/CommentedKeyword: Enabled: false Style/FormatString: EnforcedStyle: percent Style/SignalException: EnforcedStyle: semantic Metrics/BlockLength: Exclude: - '*.gemspec' - spec/**/*_spec.rb morpher-0.4.2/Changelog.md000066400000000000000000000036661420246321600154150ustar00rootroot00000000000000# v0.4.1 2021-06-23 * Add various builder methods `Morpher::Transform#{array,seq,maybe}` * Add Morpher::Transform::Maybe # v0.4.0 2021-03-07 * Change semantics of optional hash keys to aways fill `nil` valued keys. # v0.3.0 2021-02-22 * Re-implement as extraction from Morpher::Transform # v0.2.3 2014-04-22 [Compare v0.2.2..v0.2.3](https://github.com/mbj/morpher/compare/v0.2.2...v0.2.3) Changes: * Dependency updates. # v0.2.2 2014-04-10 [Compare v0.2.1..v0.2.2](https://github.com/mbj/morpher/compare/v0.2.1...v0.2.2) Changes: * Add Morpher.sexp returning a morpher AST node via evaluating a block with sexp node API available. * Add Morpher.build returning a morpher evaluator via evaluating a block with sexp node API available. * Fix evaluation errors on Transformer::Map node. * Ensure mutant coverage scores on CI # v0.2.1 2014-03-29 [Compare v0.2.0..v0.2.1](https://github.com/mbj/morpher/compare/v0.2.0...v0.2.1) Changes: * Fix warnings on multiple method definition # v0.2.0 2014-03-09 [Compare v0.1.0..v0.2.0](https://github.com/mbj/morpher/compare/v0.1.0...v0.2.0) Changes: * Add param node s(:param, Model, :some, :attributes) to build Transformer::Domain::Param Breaking-Changes: * Rename {load,dump}_attributes_hash to {load,dump}_attribute_hash * Require {load,dump}_attribute_hash param to be an instance of Transformer::Domain::Param # v0.1.0 2014-03-08 [Compare v0.0.1..v0.1.0](https://github.com/mbj/morpher/compare/v0.0.1...v0.1.0) Breaking-Changes: * Renamed `Morpher.evaluator(node)` to `Morpher.compile(node)` * Rename node: `symbolize_key` to `key_symbolize` * Rename node: `anima_load` to `load_attributes_hash` * Rename node: `anima_dump` to `dump_attributes_hash` * The ability to rescue/report anima specific exceptions has been dropped Changes: * Add {dump,load}_{attribute_accessors,instance_variables} as additional strategies to transform from / to domain objects. # v0.0.1 2014-03-02 First public release. morpher-0.4.2/Gemfile000066400000000000000000000002521420246321600144630ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec source 'https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev' do gem 'mutant-license' end morpher-0.4.2/Gemfile.lock000066400000000000000000000051041420246321600154130ustar00rootroot00000000000000PATH remote: . specs: morpher (0.4.1) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) anima (~> 0.3.0) concord (~> 0.1.5) equalizer (~> 0.0.9) mprelude (~> 0.1.0) procto (~> 0.0.2) GEM remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ specs: mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) GEM remote: https://rubygems.org/ specs: abstract_type (0.0.7) adamantium (0.2.0) ice_nine (~> 0.11.0) memoizable (~> 0.4.0) anima (0.3.2) abstract_type (~> 0.0.7) adamantium (~> 0.2) equalizer (~> 0.0.11) ast (2.4.2) concord (0.1.6) adamantium (~> 0.2.0) equalizer (~> 0.0.9) diff-lcs (1.5.0) equalizer (0.0.11) ice_nine (0.11.2) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) mprelude (0.1.0) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) concord (~> 0.1.5) equalizer (~> 0.0.9) ice_nine (~> 0.11.1) procto (~> 0.0.2) mutant (0.11.4) diff-lcs (~> 1.3) parser (~> 3.1.0) regexp_parser (~> 2.0, >= 2.0.3) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.4) mutant-rspec (0.11.4) mutant (= 0.11.4) rspec-core (>= 3.8.0, < 4.0.0) parallel (1.21.0) parser (3.1.0.0) ast (~> 2.4.1) procto (0.0.3) rainbow (3.0.0) regexp_parser (2.2.1) rexml (3.2.5) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) rspec-mocks (~> 3.10.0) rspec-core (3.10.1) rspec-support (~> 3.10.0) rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.3) rubocop (1.22.3) parallel (~> 1.10) parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml rubocop-ast (>= 1.12.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.12.0) parser (>= 3.0.1.1) ruby-progressbar (1.11.0) sorbet-runtime (0.5.9636) thread_safe (0.3.6) unicode-display_width (2.1.0) unparser (0.6.4) diff-lcs (~> 1.3) parser (>= 3.1.0) PLATFORMS ruby DEPENDENCIES morpher! mutant (~> 0.10) mutant-license! mutant-rspec (~> 0.10) rspec (~> 3.10) rspec-core (~> 3.10) rspec-its (~> 1.3.0) rubocop (~> 1.11) BUNDLED WITH 2.2.25 morpher-0.4.2/LICENSE000066400000000000000000000020411420246321600141730ustar00rootroot00000000000000Copyright (c) 2021 Markus Schirp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. morpher-0.4.2/README.md000066400000000000000000000014041420246321600144470ustar00rootroot00000000000000morpher ======= [![CI](https://github.com/mbj/morpher/actions/workflows/ci.yml/badge.svg)](https://github.com/mbj/morpher/actions/workflows/ci.yml) [![Build Status](https://secure.travis-ci.org/mbj/morpher.png?branch=master)](http://travis-ci.org/mbj/morpher) Morpher is a compsoable data transformation algebra with data trace based error reporting. It can be used at various places: * Domain to JSON and vice versa, for building rest style APIS * Domain to document db and vice versa, for building mappers * Form processing * ... Installation ------------ Install the gem `morpher` via your preferred method. Examples -------- See specs, Public Evaluator API is stable and there are ongoing 0.x.y releases for early adopters. License ------- See LICENSE file. morpher-0.4.2/config/000077500000000000000000000000001420246321600144365ustar00rootroot00000000000000morpher-0.4.2/config/devtools.yml000066400000000000000000000000331420246321600170140ustar00rootroot00000000000000--- unit_test_timeout: 1.0 morpher-0.4.2/config/flay.yml000066400000000000000000000000431420246321600161110ustar00rootroot00000000000000--- threshold: 23 total_score: 427 morpher-0.4.2/config/flog.yml000066400000000000000000000000551420246321600161100ustar00rootroot00000000000000--- threshold: 27.0 # TODO: decrease to ~ 10 morpher-0.4.2/config/heckle.yml000066400000000000000000000000561420246321600164150ustar00rootroot00000000000000--- library: adamantium namespace: Adamantium morpher-0.4.2/config/mutant.yml000066400000000000000000000001111420246321600164620ustar00rootroot00000000000000integration: rspec requires: - morpher matcher: subjects: - Morpher* morpher-0.4.2/config/reek.yml000066400000000000000000000047031420246321600161130ustar00rootroot00000000000000--- UncommunicativeParameterName: accept: [] exclude: [] enabled: true reject: - !ruby/regexp /^.$/ - !ruby/regexp /[0-9]$/ - !ruby/regexp /[A-Z]/ TooManyMethods: max_methods: 10 exclude: - Morpher::Printer # 16 methods enabled: true max_instance_variables: 2 UncommunicativeMethodName: accept: ['s'] exclude: [] enabled: true reject: - !ruby/regexp /^[a-z]$/ - !ruby/regexp /[0-9]$/ - !ruby/regexp /[A-Z]/ LongParameterList: max_params: 3 # TODO: decrease max_params to 2 exclude: [] enabled: true overrides: {} FeatureEnvy: exclude: # False positives: - Morpher::Printer::Mixin::InstanceMethods#description - Morpher::Evaluator::Transformer::Domain::AttributeHash::Dump#call - Morpher::Evaluator::Transformer::Domain::AttributeAccessors::Load#call - Morpher::Evaluator::Transformer::Domain::InstanceVariables::Load#call - Morpher::Evaluator::Transformer::Domain::InstanceVariables::Dump#call enabled: true ClassVariable: exclude: [] enabled: true BooleanParameter: exclude: [] enabled: true IrresponsibleModule: exclude: # False positives - Morpher::Compiler - Morpher::Compiler::Preprocessor - Morpher::Compiler::Evaluator enabled: true UncommunicativeModuleName: accept: [] exclude: [] enabled: true reject: - !ruby/regexp /^.$/ - !ruby/regexp /[0-9]$/ NestedIterators: ignore_iterators: [] exclude: [] enabled: true max_allowed_nesting: 2 TooManyStatements: max_statements: 7 # TODO: decrease max_statements to 5 or less exclude: - Morpher::Compiler::Emitter#self.children enabled: true DuplicateMethodCall: allow_calls: [] exclude: [] enabled: false # TOOD enable max_calls: 1 UtilityFunction: max_helper_calls: 1 exclude: - Morpher::Evaluator::Predicate::Contradiction#inverse - Morpher::Evaluator::Predicate::Tautology#inverse - Morpher::Evaluator::Transformer::Coerce::ParseIso8601DateTime#invoke - Morpher::Evaluator::Transformer::Domain::AttributeHash::Dump#call enabled: true Attribute: exclude: [] enabled: false UncommunicativeVariableName: accept: ['_'] exclude: [] enabled: true reject: - !ruby/regexp /^.$/ - !ruby/regexp /[0-9]$/ - !ruby/regexp /[A-Z]/ RepeatedConditional: exclude: [] enabled: true max_ifs: 2 DataClump: exclude: [] enabled: true max_copies: 1 min_clump_size: 3 ControlParameter: exclude: [] enabled: true LongYieldList: max_params: 1 exclude: [] enabled: true NilCheck: exclude: [] morpher-0.4.2/config/yardstick.yml000066400000000000000000000000231420246321600171510ustar00rootroot00000000000000--- threshold: 100 morpher-0.4.2/lib/000077500000000000000000000000001420246321600137375ustar00rootroot00000000000000morpher-0.4.2/lib/morpher.rb000066400000000000000000000004331420246321600157400ustar00rootroot00000000000000# frozen_string_literal: true require 'abstract_type' require 'adamantium' require 'anima' require 'concord' require 'mprelude' module Morpher Either = MPrelude::Either EMPTY_HASH = {}.freeze end require 'morpher/newtype' require 'morpher/record' require 'morpher/transform' morpher-0.4.2/lib/morpher/000077500000000000000000000000001420246321600154135ustar00rootroot00000000000000morpher-0.4.2/lib/morpher/newtype.rb000066400000000000000000000011411420246321600174300ustar00rootroot00000000000000# frozen_string_literal: true module Morpher # Generator for primitive wrappers class Newtype < Module include Concord.new(:transform) # rubocop:disable Metrics/MethodLength def included(host) transform = transform() host.class_eval do include Adamantium::Flat, Concord::Public.new(:value) const_set( :TRANSFORM, Transform::Sequence.new( [ transform, Transform::Success.new(public_method(:new)) ] ) ) end end # rubocop:enable Metrics/MethodLength end end morpher-0.4.2/lib/morpher/record.rb000066400000000000000000000025621420246321600172230ustar00rootroot00000000000000# frozen_string_literal: true module Morpher # Generator for struct a-like wrappers class Record < Module DEFAULTS = { required: EMPTY_HASH, optional: EMPTY_HASH }.freeze include Anima.new(:required, :optional) def self.new(**attributes) super(DEFAULTS.merge(attributes)) end # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength def included(host) optional = optional() optional_transform = transform(optional) required = required() required_transform = transform(required) host.class_eval do include Adamantium::Flat, Anima.new(*(required.keys + optional.keys)) const_set( :TRANSFORM, Transform::Sequence.new( [ Transform::Primitive.new(Hash), Transform::Hash::Symbolize.new, Transform::Hash.new( required: required_transform, optional: optional_transform ), Transform::Success.new(public_method(:new)) ] ) ) end end # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/MethodLength private def transform(attributes) attributes.map do |name, transform| Transform::Hash::Key.new(name, transform) end end end # Record end # Morpher morpher-0.4.2/lib/morpher/transform.rb000066400000000000000000000274501420246321600177630ustar00rootroot00000000000000# frozen_string_literal: true module Morpher # Composable transform declaration and execution class Transform include AbstractType include Adamantium # Default slug # # @return [String] def slug self.class.to_s end # Apply transformation to input # # @param [Object] input # # @return [Either] abstract_method :call # Build sequence # # @param [Transform] transform # # @return [Transform] def seq(transform) Sequence.new([self, transform]) end # Build array transform # # @return [Transform] def array Array.new(self) end # Build maybe transform # # @return [Transform] def maybe Maybe.new(self) end # Build Proc to transform input # # @return [Proc] def to_proc public_method(:call).to_proc end # Deep error data structure class Error include Anima.new( :cause, :input, :message, :transform ) include Adamantium COMPACT = '%s: %s' private_constant(*constants(false)) # Compact error message # # @return [String] def compact_message COMPACT % { path: path, message: trace.last.message } end memoize :compact_message # Error path trace # # @return [Array] def trace [self, *cause&.trace] end memoize :trace private def path trace.map { |error| error.transform.slug }.reject(&:empty?).join('/') end end # Error # Wrapper adding a name to a transformation class Named < self include Concord.new(:name, :transform) # Apply transformation to input # # @return [Either] def call(input) transform.call(input).lmap(&method(:wrap_error)) end # Named slug # # @return [String] def slug name end end # Named # Transform based on a (captured) block with added name class Block < self include Anima.new(:block, :name) def self.capture(name, &block) new(block: block, name: name) end def call(input) block .call(input) .lmap do |message| Error.new( cause: nil, input: input, message: message, transform: self ) end end def slug name end end private def error(input:, cause: nil, message: nil) Error.new( cause: cause, input: input, message: message, transform: self ) end def lift_error(error) error.with(transform: self) end def wrap_error(error) error(cause: error, input: error.input) end def failure(value) Either::Left.new(value) end def success(value) Either::Right.new(value) end # Index attached to a transform class Index < self include Anima.new(:index, :transform) private(*anima.attribute_names) # rubocop:disable Style/AccessModifierDeclarations # Create error at specified index # # @param [Error] cause # @param [Integer] index # # @return [Error] def self.wrap(cause, index) Error.new( cause: cause, input: cause.input, message: nil, transform: new(index: index, transform: cause.transform) ) end # Apply transformation to input # # @param [Object] input # # @return [Either] def call(input) transform.call(input).lmap(&method(:wrap_error)) end # Rendering slug # # @return [Array] def slug '%d' % { index: index } end memoize :slug end # Index # Transform guarding a specific primitive class Primitive < self include Concord.new(:primitive) MESSAGE = 'Expected: %s but got: %s' private_constant(*constants(false)) # Apply transformation to input # # @param [Object] input # # @return [Either] def call(input) if input.instance_of?(primitive) success(input) else failure( error( input: input, message: MESSAGE % { actual: input.class, expected: primitive } ) ) end end # Rendering slug # # @return [String] def slug primitive.to_s end memoize :slug end # Primitive # Transform guarding boolean primitives class Boolean < self include Concord.new MESSAGE = 'Expected: boolean but got: %s' private_constant(*constants(false)) # Apply transformation to input # # @param [Object] input # # @return [Either] def call(input) if input.equal?(true) || input.equal?(false) success(input) else failure( error( message: MESSAGE % { actual: input.inspect }, input: input ) ) end end end # Boolean # Transform an array via mapping it over transform class Array < self include Concord.new(:transform) MESSAGE = 'Failed to coerce array at index: %d' PRIMITIVE = Primitive.new(::Array) private_constant(*constants(false)) # Apply transformation to input # # @param [Object] input # # @return [Either>] def call(input) PRIMITIVE .call(input) .lmap(&method(:lift_error)) .bind(&method(:run)) end private # rubocop:disable Metrics/MethodLength def run(input) output = [] input.each_with_index do |value, index| output << transform.call(value).lmap do |error| return failure( error( cause: Index.wrap(error, index), message: MESSAGE % { index: index }, input: input ) ) end.from_right end success(output) end # rubocop:enable Metrics/MethodLength end # Array # Transform a hash via mapping it over key specific transforms class Hash < self include Anima.new(:optional, :required) KEY_MESSAGE = 'Missing keys: %s, Unexpected keys: %s' PRIMITIVE = Primitive.new(::Hash) private_constant(*constants(false)) # Transform to symbolize array keys class Symbolize < Transform include Equalizer.new # Apply transformation to input # # @param [Hash{String => Object}] # # @return [Hash{Symbol => Object}] def call(input) unless input.keys.all? { |key| key.instance_of?(String) } return failure(error(input: input, message: 'Found non string key in input')) end success(input.transform_keys(&:to_sym)) end end # Symbolize # Key specific transformation class Key < Transform include Concord::Public.new(:value, :transform) # Rendering slug # # @return [String] def slug '[%s]' % { key: value.inspect } end memoize :slug # Apply transformation to input # # @param [Object] # # @return [Either] def call(input) transform.call(input).lmap do |error| error(cause: error, input: input) end end end # Key # Apply transformation to input # # @param [Object] input # # @return [Either] def call(input) PRIMITIVE .call(input) .lmap(&method(:lift_error)) .bind(&method(:reject_keys)) .bind(&method(:transform)) end private def transform(input) transform_required(input).bind do |required| transform_optional(input).fmap(&required.public_method(:merge)) end end def transform_required(input) transform_keys(required, input) end def defaults optional.map(&:value).product([nil]).to_h end memoize :defaults def transform_optional(input) transform_keys( optional.select { |key| input.key?(key.value) }, input ).fmap(&defaults.public_method(:merge)) end # rubocop:disable Metrics/MethodLength def transform_keys(keys, input) success( keys .map do |key| [ key.value, coerce_key(key, input).from_right do |error| return failure(error) end ] end .to_h ) end # rubocop:enable Metrics/MethodLength def coerce_key(key, input) key.call(input.fetch(key.value)).lmap do |error| error(input: input, cause: error) end end # rubocop:disable Metrics/MethodLength def reject_keys(input) keys = input.keys unexpected = keys - allowed_keys missing = required_keys - keys if unexpected.empty? && missing.empty? success(input) else failure( error( input: input, message: KEY_MESSAGE % { missing: missing, unexpected: unexpected } ) ) end end # rubocop:enable Metrics/MethodLength def allowed_keys required_keys + optional.map(&:value) end memoize :allowed_keys def required_keys required.map(&:value) end memoize :required_keys end # Hash # Sequence of transformations class Sequence < self include Concord.new(:steps) # Build sequence # # @param [Transform] transform # # @return [Transform] def seq(transform) self.class.new(steps + [transform]) end # Apply transformation to input # # @param [Object] # # @return [Either] def call(input) current = input steps.each_with_index do |step, index| current = step.call(current).from_right do |error| return failure(error(cause: Index.wrap(error, index), input: input)) end end success(current) end end # Sequence # Generic exception transform class Exception < self include Concord.new(:error_class, :block) # Apply transformation to input # # @param [Object] # # @return [Either] def call(input) Either .wrap_error(error_class) { block.call(input) } .lmap { |exception| error(input: input, message: exception.to_s) } end end # Exception # Transform sucessfully class Success < self include Concord.new(:block) # Apply transformation to input # # @param [Object] # # @return [Either] def call(input) success(block.call(input)) end end # Success # Transform accepting nil values class Maybe < Transform include Concord.new(:transform) def call(input) if input.nil? success(nil) else transform.call(input).lmap(&method(:wrap_error)) end end end # Maybe BOOLEAN = Transform::Boolean.new FLOAT = Transform::Primitive.new(Float) INTEGER = Transform::Primitive.new(Integer) STRING = Transform::Primitive.new(String) STRING_ARRAY = Transform::Array.new(STRING) end # Transform end # Morpher morpher-0.4.2/morpher.gemspec000066400000000000000000000024261420246321600162160ustar00rootroot00000000000000# frozen_string_literal: true Gem::Specification.new do |gem| gem.name = 'morpher' gem.version = '0.4.1' gem.authors = ['Markus Schirp'] gem.email = 'mbj@schirp-dso.com' gem.description = 'Domain Transformation Algebra' gem.summary = gem.description gem.homepage = 'https://github.com/mbj/morpher' gem.require_paths = %w[lib] gem.files = Dir.glob('lib/**/*') gem.extra_rdoc_files = %w[LICENSE] gem.license = 'MIT' gem.required_ruby_version = '>= 2.6' gem.add_runtime_dependency('abstract_type', '~> 0.0.7') gem.add_runtime_dependency('adamantium', '~> 0.2.0') gem.add_runtime_dependency('anima', '~> 0.3.0') gem.add_runtime_dependency('concord', '~> 0.1.5') gem.add_runtime_dependency('equalizer', '~> 0.0.9') gem.add_runtime_dependency('mprelude', '~> 0.1.0') gem.add_runtime_dependency('procto', '~> 0.0.2') gem.add_development_dependency('mutant', '~> 0.10') gem.add_development_dependency('mutant-rspec', '~> 0.10') gem.add_development_dependency('rspec', '~> 3.10') gem.add_development_dependency('rspec-core', '~> 3.10') gem.add_development_dependency('rspec-its', '~> 1.3.0') gem.add_development_dependency('rubocop', '~> 1.11') end morpher-0.4.2/spec/000077500000000000000000000000001420246321600141235ustar00rootroot00000000000000morpher-0.4.2/spec/spec_helper.rb000066400000000000000000000004111420246321600167350ustar00rootroot00000000000000# frozen_string_literal: true require 'morpher' module PreludeHelper def right(value) Morpher::Either::Right.new(value) end def left(value) Morpher::Either::Left.new(value) end end RSpec.configure do |config| config.include(PreludeHelper) end morpher-0.4.2/spec/unit/000077500000000000000000000000001420246321600151025ustar00rootroot00000000000000morpher-0.4.2/spec/unit/morpher/000077500000000000000000000000001420246321600165565ustar00rootroot00000000000000morpher-0.4.2/spec/unit/morpher/newtype_spec.rb000066400000000000000000000031051420246321600216070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Newtype do describe '#included' do define_method(:apply) do instance = instance() host.class_eval { include instance } end let(:host) { Class.new } let(:host_instance) { host.new(value) } let(:instance) { described_class.new(transform) } let(:transform) { Morpher::Transform::Primitive.new(String) } let(:value) { instance_double(Object, :value) } it 'creates public #value method on host instances' do apply expect(host_instance.value).to be(value) end it 'includes Adamantium::Flat into the host' do apply expect(host.ancestors).to include(Adamantium::Flat) expect(host_instance.frozen?).to be(true) end it 'allows to equalize on host instances' do apply expect(host_instance.eql?(host.new(value))).to be(true) end it 'creates the expected TRANSFORMER constant on the host' do apply expect(host::TRANSFORM).to eql( Morpher::Transform::Sequence.new( [ transform, Morpher::Transform::Success.new(host.method(:new)) ] ) ) end it 'does not create the #value method on the instance' do apply expect(instance.instance_methods).to_not include(:value) end it 'does not create the TRANSFORMER constant on the instance' do apply expect(instance.constants).to_not include(:TRANSFORMER) end end end morpher-0.4.2/spec/unit/morpher/record_spec.rb000066400000000000000000000065321420246321600214010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Record do describe '.new' do def apply described_class.new(**attributes) end let(:fields) { { foo: Morpher::Transform::STRING } } context 'on absent :optional' do let(:attributes) { { required: fields } } it 'defaults to empty optional' do expect(apply.to_h).to eql( optional: {}, required: fields ) end end context 'on absent :required' do let(:attributes) { { optional: fields } } it 'defaults to empty optional' do expect(apply.to_h).to eql( optional: fields, required: {} ) end end context 'on present :required and :optional' do let(:attributes) do { optional: { foo: Morpher::Transform::STRING }, required: { bar: Morpher::Transform::STRING } } end it 'does not apply defaults' do expect(apply.to_h).to eql(attributes) end end end describe '#included' do define_method(:apply) do instance = instance() host.class_eval { include instance } end let(:attributes) { { a: 'foo', b: 10, c: nil } } let(:host) { Class.new } let(:host_instance) { host.new(attributes) } let(:instance) do described_class.new( required: required_transform, optional: optional_transform ) end let(:required_transform) do { a: Morpher::Transform::Primitive.new(String), b: Morpher::Transform::Primitive.new(Integer) } end let(:optional_transform) do { c: Morpher::Transform::Boolean.new } end let(:expected_required_keys_transform) do [ Morpher::Transform::Hash::Key.new(:a, required_transform.fetch(:a)), Morpher::Transform::Hash::Key.new(:b, required_transform.fetch(:b)) ] end let(:expected_optional_keys_transform) do [ Morpher::Transform::Hash::Key.new(:c, optional_transform.fetch(:c)) ] end it 'creates public #to_h method on host instances' do apply expect(host_instance.to_h).to eql(attributes) end it 'includes Adamantium::Flat into the host' do apply expect(host.ancestors).to include(Adamantium::Flat) expect(host_instance.frozen?).to be(true) end it 'allows to equalize on host instances' do apply expect(host_instance.eql?(host.new(attributes))).to be(true) end it 'creates the expected TRANSFORMER constant on the host' do apply expect(host::TRANSFORM).to eql( Morpher::Transform::Sequence.new( [ Morpher::Transform::Primitive.new(Hash), Morpher::Transform::Hash::Symbolize.new, Morpher::Transform::Hash.new( required: expected_required_keys_transform, optional: expected_optional_keys_transform ), Morpher::Transform::Success.new(host.method(:new)) ] ) ) end it 'does not create the #value method on the instance' do apply expect(instance.instance_methods).to_not include(:value) end it 'does not create the TRANSFORMER constant on the instance' do apply expect(instance.constants).to_not include(:TRANSFORMER) end end end morpher-0.4.2/spec/unit/morpher/transform/000077500000000000000000000000001420246321600205715ustar00rootroot00000000000000morpher-0.4.2/spec/unit/morpher/transform/array_spec.rb000066400000000000000000000042531420246321600232520ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Array do subject { described_class.new(transform) } let(:transform) { Morpher::Transform::Boolean.new } describe '#call' do def apply subject.call(input) end context 'on array input' do context 'empty' do let(:input) { [] } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'valid elements' do let(:input) { [true, true] } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'invalid elements' do let(:input) { [true, 1] } let(:boolean_error) do Morpher::Transform::Error.new( cause: nil, input: 1, message: 'Expected: boolean but got: 1', transform: transform ) end let(:index_error) do Morpher::Transform::Error.new( cause: boolean_error, input: 1, message: nil, transform: Morpher::Transform::Index.new(index: 1, transform: transform) ) end let(:error) do Morpher::Transform::Error.new( cause: index_error, input: input, message: 'Failed to coerce array at index: 1', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end context 'transformed elements' do let(:input) { [{ 'foo' => 'bar' }] } let(:transform) { Morpher::Transform::Hash::Symbolize.new } it 'returns transformed elements' do expect(apply).to eql(right([foo: 'bar'])) end end end context 'on other input' do let(:input) { false } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: Array but got: FalseClass', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end morpher-0.4.2/spec/unit/morpher/transform/block_spec.rb000066400000000000000000000023101420246321600232160ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Block do subject { described_class.new(name: name, block: block) } let(:block) { ->(value) { right(value * 2) } } let(:name) { :external } describe '#call' do def apply subject.call(input) end let(:input) { 3 } context 'when block suceeds' do it 'returns success' do expect(apply).to eql(right(6)) end end context 'when block fails' do let(:block) { ->(_value) { left('some error') } } it 'returns expected error' do expect(apply).to eql( left( Morpher::Transform::Error.new( cause: nil, input: input, message: 'some error', transform: subject ) ) ) end end end describe '#slug' do def apply subject.slug end it 'returns name' do expect(apply).to be(name) end end describe '.capture' do def apply described_class.capture(name, &block) end it 'returns expected transform' do expect(apply).to eql(described_class.new(name: name, block: block)) end end end morpher-0.4.2/spec/unit/morpher/transform/bool_spec.rb000066400000000000000000000023011420246321600230570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Boolean do subject { described_class.new } describe '#call' do def apply subject.call(input) end context 'on true' do let(:input) { true } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'on false' do let(:input) { false } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'on nil input' do let(:input) { nil } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: boolean but got: nil', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end context 'on truthy input' do let(:input) { '' } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: boolean but got: ""', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end morpher-0.4.2/spec/unit/morpher/transform/error_spec.rb000066400000000000000000000061601420246321600232640ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Error do subject { described_class.new(attributes) } let(:message) { 'root-message' } let(:direct_cause) { nil } let(:indirect_cause) { nil } let(:attributes) do transform = if direct_cause Morpher::Transform::Named.new('root', direct_cause.transform) else Morpher::Transform::Boolean.new end { cause: direct_cause, input: nil, message: message, transform: transform } end shared_context 'direct cause' do let(:direct_cause) do transform = if indirect_cause Morpher::Transform::Named.new('direct-cause', indirect_cause.transform) else Morpher::Transform::Boolean.new end described_class.new( cause: indirect_cause, input: nil, message: 'direct-cause-message', transform: transform ) end end shared_examples 'indirect cause' do let(:indirect_cause) do described_class.new( cause: nil, input: nil, message: 'indirect-cause-message', transform: Morpher::Transform::Boolean.new ) end end describe '#trace' do def apply subject.trace end context 'without cause' do it 'returns path to self' do expect(apply).to eql([subject]) end end context 'with direct cause' do include_context 'direct cause' it 'returns path to direct cause' do expect(apply).to eql([subject, direct_cause]) end end context 'with indirect cause' do include_context 'direct cause' include_context 'indirect cause' it 'returns path to direct cause' do expect(apply).to eql([subject, direct_cause, indirect_cause]) end end end describe '#compact_message' do def apply subject.compact_message end context 'root cause' do it 'returns expected message' do expect(apply).to eql('Morpher::Transform::Boolean: root-message') end end context 'with direct cause' do include_context 'direct cause' it 'returns expected message' do expect(apply).to eql(<<~'MESSAGE'.chomp) root/Morpher::Transform::Boolean: direct-cause-message MESSAGE end end context 'with indirect cause' do include_context 'direct cause' include_context 'indirect cause' context 'with present slugs' do it 'returns expected message' do expect(apply).to eql(<<~'MESSAGE'.chomp) root/direct-cause/Morpher::Transform::Boolean: indirect-cause-message MESSAGE end end context 'with empty slug' do let(:direct_cause) do super().with( transform: Morpher::Transform::Named.new('', indirect_cause.transform) ) end it 'returns expected message' do expect(apply).to eql(<<~'MESSAGE'.chomp) root/Morpher::Transform::Boolean: indirect-cause-message MESSAGE end end end end end morpher-0.4.2/spec/unit/morpher/transform/exception_spec.rb000066400000000000000000000017161420246321600241330ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Exception do subject { described_class.new(error_class, block) } let(:error_class) do Class.new(RuntimeError) end describe '#call' do def apply subject.call(input) end let(:input) { 2 } context 'block that does not raise' do let(:block) { ->(input) { input * input } } it 'returns expected success value' do expect(apply).to eql(right(4)) end end context 'on block that raises' do context 'a covered exception' do let(:block) { ->(_input) { fail(error_class, 'some message') } } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'some message', transform: subject ) end it 'returns expected error' do expect(apply).to eql(left(error)) end end end end end morpher-0.4.2/spec/unit/morpher/transform/hash_spec.rb000066400000000000000000000137361420246321600230650ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Hash do subject { described_class.new(attributes) } let(:required) { [] } let(:optional) { [] } let(:symbol) { Morpher::Transform::Primitive.new(Symbol) } let(:attributes) do { required: required, optional: optional } end describe '#call' do def apply subject.call(input) end context 'on Hash input' do context 'empty' do let(:input) { {} } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'missing key' do let(:input) { {} } let(:required) { [described_class::Key.new(:foo, symbol)] } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Missing keys: [:foo], Unexpected keys: []', transform: subject ) end it 'returns error' do expect(apply).to eql(left(error)) end end context 'extra key' do let(:input) { { foo: :bar } } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Missing keys: [], Unexpected keys: [:foo]', transform: subject ) end it 'returns error' do expect(apply).to eql(left(error)) end end context 'using required' do let(:input) { { foo: :bar } } let(:required) { [described_class::Key.new(:foo, symbol)] } it 'returns success' do expect(apply).to eql(right(input)) end end context 'using optional' do let(:optional) { [described_class::Key.new(:foo, symbol)] } context 'not providing the optional key' do let(:input) { {} } it 'returns success' do expect(apply).to eql(right(foo: nil)) end end context 'providing the optional key' do let(:input) { { foo: :bar } } it 'returns success' do expect(apply).to eql(right(input)) end end end shared_examples 'key transform error' do let(:innermost_error) do Morpher::Transform::Error.new( cause: nil, input: 'bar', message: 'Expected: Symbol but got: String', transform: symbol ) end let(:inner_error) do Morpher::Transform::Error.new( cause: innermost_error, input: 'bar', message: nil, transform: key_transform ) end let(:error) do Morpher::Transform::Error.new( cause: inner_error, input: input, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end context 'key transform error' do let(:input) { { foo: 'bar' } } let(:key_transform) { described_class::Key.new(:foo, symbol) } context 'on optional key' do let(:optional) { [key_transform] } include_examples 'key transform error' end context 'on required key' do let(:required) { [key_transform] } include_examples 'key transform error' end end end context 'on other input' do let(:input) { [] } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: Hash but got: Array', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end RSpec.describe Morpher::Transform::Hash::Symbolize do subject { described_class.new } describe '#call' do def apply subject.call(input) end context 'on all string keys' do let(:input) { { 'foo' => 'bar' } } it 'returns success' do expect(apply).to eql(right(foo: 'bar')) end end context 'on non string keys' do let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Found non string key in input', transform: subject ) end context 'one non string key' do let(:input) { { 1 => 'bar' } } it 'returns error' do expect(apply).to eql(left(error)) end end context 'one non string key next to string key' do let(:input) { { 1 => 'bar', 'foo' => 'baz' } } it 'returns error' do expect(apply).to eql(left(error)) end end end end end RSpec.describe Morpher::Transform::Hash::Key do subject { described_class.new(:foo, boolean) } let(:boolean) { Morpher::Transform::Boolean.new } describe '#slug' do def apply subject.slug end it 'returns expected slug' do expect(apply).to eql('[:foo]') end end describe '#call' do def apply subject.call(input) end context 'on valid input' do let(:input) { true } it 'returns success' do expect(apply).to eql(right(true)) end end context 'on invalid input' do let(:input) { 1 } let(:inner_error) do Morpher::Transform::Error.new( cause: nil, input: 1, message: 'Expected: boolean but got: 1', transform: boolean ) end let(:error) do Morpher::Transform::Error.new( cause: inner_error, input: 1, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end morpher-0.4.2/spec/unit/morpher/transform/index_spec.rb000066400000000000000000000035741420246321600232500ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Index do subject { described_class.new(index: index, transform: transform) } let(:index) { 1 } let(:transform) { Morpher::Transform::Boolean.new } describe '#slug' do def apply subject.slug end it 'retursn expected value' do expect(apply).to eql('1') end it 'returns frozen value' do expect(apply.frozen?).to be(true) end it 'returns idempotent value' do expect(apply).to be(apply) end end describe '#call' do def apply subject.call(input) end context 'on valid input' do let(:input) { true } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'on nvalid input' do let(:input) { 1 } let(:boolean_error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: boolean but got: 1', transform: transform ) end let(:error) do Morpher::Transform::Error.new( cause: boolean_error, input: input, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end describe '.wrap' do def apply described_class.wrap(error, index) end let(:error) do Morpher::Transform::Error.new( cause: :nil, input: 1, message: nil, transform: transform ) end it 'returns wrapped error' do expect(apply).to eql( Morpher::Transform::Error.new( cause: error, input: 1, message: nil, transform: described_class.new(index: index, transform: transform) ) ) end end end morpher-0.4.2/spec/unit/morpher/transform/maybe_spec.rb000066400000000000000000000020401420246321600232210ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Maybe do subject { described_class.new(transform) } let(:transform) { Morpher::Transform::STRING } describe '#call' do def apply subject.call(input) end context 'on nil input' do let(:input) { nil } it 'returns sucess' do expect(apply).to eql(right(nil)) end end context 'on non nil input' do context 'on input valid for innner transform' do let(:input) { 'some-string' } it 'returns sucess' do expect(apply).to eql(right('some-string')) end end context 'on input invalud for inner transform' do let(:input) { 1 } let(:error) do Morpher::Transform::Error.new( cause: transform.call(input).from_left, input: input, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end end morpher-0.4.2/spec/unit/morpher/transform/named_spec.rb000066400000000000000000000017121420246321600232150ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Named do subject { described_class.new(name, transform) } let(:name) { 'transform-name' } let(:transform) { Morpher::Transform::Boolean.new } describe '#slug' do def apply subject.slug end it 'returns name' do expect(apply).to be(name) end end describe '#call' do def apply subject.call(input) end context 'on valid input' do let(:input) { true } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'on invalid input' do let(:input) { 1 } let(:error) do Morpher::Transform::Error.new( cause: transform.call(input).from_left, input: input, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end morpher-0.4.2/spec/unit/morpher/transform/primitive_spec.rb000066400000000000000000000020461420246321600241420ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Primitive do subject { described_class.new(primitive) } let(:primitive) { String } describe '#slug' do def apply subject.slug end it 'returns strigified primitive' do expect(apply).to eql('String') end it 'is idempotent' do expect(apply).to be(apply) end it 'is frozen' do expect(apply.frozen?).to be(true) end end describe '#call' do def apply subject.call(input) end context 'on string input' do let(:input) { 'some-string' } it 'returns sucess' do expect(apply).to eql(right(input)) end end context 'on other input' do let(:input) { 1 } let(:error) do Morpher::Transform::Error.new( cause: nil, input: input, message: 'Expected: String but got: Integer', transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end end morpher-0.4.2/spec/unit/morpher/transform/sequence_spec.rb000066400000000000000000000047331420246321600237470ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Sequence do subject { described_class.new(steps) } let(:steps) do [ hash_transform, hash_symbolize ] end let(:hash_transform) do Morpher::Transform::Hash.new(optional: [], required: [key_transform]) end let(:key_transform) do Morpher::Transform::Hash::Key.new('foo', Morpher::Transform::Boolean.new) end let(:boolean_transform) do Morpher::Transform::Boolean.new end let(:hash_symbolize) do Morpher::Transform::Hash::Symbolize.new end describe '#call' do def apply subject.call(input) end context 'on valid input' do let(:input) { { 'foo' => true } } it 'returns success' do expect(apply).to eql(right(foo: true)) end end context 'on invalid input' do let(:input) { { 'foo' => 1 } } let(:boolean_error) do Morpher::Transform::Error.new( cause: nil, input: 1, message: 'Expected: boolean but got: 1', transform: boolean_transform ) end let(:key_error) do Morpher::Transform::Error.new( cause: boolean_error, input: 1, message: nil, transform: key_transform ) end let(:hash_error) do Morpher::Transform::Error.new( cause: key_error, input: input, message: nil, transform: hash_transform ) end let(:index_error) do Morpher::Transform::Error.new( cause: hash_error, input: input, message: nil, transform: Morpher::Transform::Index.new( index: 0, transform: hash_transform ) ) end let(:error) do Morpher::Transform::Error.new( cause: index_error, input: input, message: nil, transform: subject ) end it 'returns failure' do expect(apply).to eql(left(error)) end end end describe '#seq' do subject do described_class.new([Morpher::Transform::STRING]) end let(:other) do Morpher::Transform::Success.new(:upcase.to_proc) end it 'build flat sequence' do expect(subject.seq(other)).to eql( described_class.new( [ Morpher::Transform::STRING, other ] ) ) end end end morpher-0.4.2/spec/unit/morpher/transform/succes_spec.rb000066400000000000000000000006271420246321600234220ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform::Success do subject { described_class.new(block) } let(:block) { ->(value) { value.to_s } } describe '#call' do def apply subject.call(input) end context 'on any input' do let(:input) { 100 } it 'returns sucess on block output' do expect(apply).to eql(right('100')) end end end end morpher-0.4.2/spec/unit/morpher/transform_spec.rb000066400000000000000000000024031420246321600221270ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Morpher::Transform do describe '#array' do subject do Morpher::Transform::STRING end it 'returns array' do expect(subject.array).to eql( Morpher::Transform::Array.new(subject) ) end end describe '#maybe' do subject do Morpher::Transform::STRING end it 'returns maybe' do expect(subject.maybe).to eql( Morpher::Transform::Maybe.new(subject) ) end end describe '#seq' do subject do Morpher::Transform::STRING end let(:other) do Morpher::Transform::Success.new(:upcase.to_proc) end it 'returns sequence' do expect(subject.seq(other)).to eql( Morpher::Transform::Sequence.new([subject, other]) ) end end describe '#to_proc' do subject do Morpher::Transform::STRING end it 'returns proc' do expect(subject.to_proc).to be_instance_of(Proc) end it 'executes #call when evaluated' do aggregate_failures do expect(subject.to_proc.call('a').from_right).to be('a') expect(subject.to_proc.call(1).lmap(&:compact_message).from_left).to eql( 'String: Expected: String but got: Integer' ) end end end end