Repository: dry-rb/dry-logic Branch: main Commit: 3326aefba07b Files: 133 Total size: 172.7 KB Directory structure: gitextract_njl1vvvj/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── config.yml │ ├── SUPPORT.md │ └── workflows/ │ ├── ci-lint.yml │ ├── ci.yml │ ├── pr-comments.yml │ ├── repo-sync-preview.yml │ └── rubocop.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks/ │ ├── builder.rb │ ├── rule_application.rb │ └── setup.rb ├── bin/ │ ├── .gitkeep │ └── console ├── dry-logic.gemspec ├── examples/ │ └── basic.rb ├── lib/ │ ├── dry/ │ │ ├── logic/ │ │ │ ├── appliable.rb │ │ │ ├── builder.rb │ │ │ ├── evaluator.rb │ │ │ ├── operations/ │ │ │ │ ├── abstract.rb │ │ │ │ ├── and.rb │ │ │ │ ├── attr.rb │ │ │ │ ├── binary.rb │ │ │ │ ├── check.rb │ │ │ │ ├── each.rb │ │ │ │ ├── implication.rb │ │ │ │ ├── key.rb │ │ │ │ ├── negation.rb │ │ │ │ ├── or.rb │ │ │ │ ├── set.rb │ │ │ │ ├── unary.rb │ │ │ │ └── xor.rb │ │ │ ├── operators.rb │ │ │ ├── predicates.rb │ │ │ ├── result.rb │ │ │ ├── rule/ │ │ │ │ ├── interface.rb │ │ │ │ └── predicate.rb │ │ │ ├── rule.rb │ │ │ ├── rule_compiler.rb │ │ │ └── version.rb │ │ └── logic.rb │ └── dry-logic.rb ├── repo-sync.yml ├── spec/ │ ├── integration/ │ │ ├── builder/ │ │ │ ├── operation_spec.rb │ │ │ └── predicate_spec.rb │ │ ├── result_spec.rb │ │ └── rule_spec.rb │ ├── shared/ │ │ ├── built_rule.rb │ │ ├── operation.rb │ │ ├── predicate.rb │ │ ├── predicates.rb │ │ └── rule.rb │ ├── spec_helper.rb │ ├── support/ │ │ ├── coverage.rb │ │ ├── mutant.rb │ │ ├── rspec.rb │ │ └── warnings.rb │ └── unit/ │ ├── builder_spec.rb │ ├── operations/ │ │ ├── and_spec.rb │ │ ├── attr_spec.rb │ │ ├── check_spec.rb │ │ ├── each_spec.rb │ │ ├── implication_spec.rb │ │ ├── key_spec.rb │ │ ├── negation_spec.rb │ │ ├── or_spec.rb │ │ ├── set_spec.rb │ │ └── xor_spec.rb │ ├── predicates/ │ │ ├── array_spec.rb │ │ ├── attr_spec.rb │ │ ├── bool_spec.rb │ │ ├── bytesize_spec.rb │ │ ├── case_spec.rb │ │ ├── date_spec.rb │ │ ├── date_time_spec.rb │ │ ├── decimal_spec.rb │ │ ├── empty_spec.rb │ │ ├── eql_spec.rb │ │ ├── even_spec.rb │ │ ├── excluded_from_spec.rb │ │ ├── excludes_spec.rb │ │ ├── false_spec.rb │ │ ├── filled_spec.rb │ │ ├── float_spec.rb │ │ ├── format_spec.rb │ │ ├── gt_spec.rb │ │ ├── gteq_spec.rb │ │ ├── hash_spec.rb │ │ ├── included_in_spec.rb │ │ ├── includes_spec.rb │ │ ├── int_spec.rb │ │ ├── key_spec.rb │ │ ├── lt_spec.rb │ │ ├── lteq_spec.rb │ │ ├── max_bytesize_spec.rb │ │ ├── max_size_spec.rb │ │ ├── min_bytesize_spec.rb │ │ ├── min_size_spec.rb │ │ ├── none_spec.rb │ │ ├── not_eql_spec.rb │ │ ├── number_spec.rb │ │ ├── odd_spec.rb │ │ ├── respond_to_spec.rb │ │ ├── size_spec.rb │ │ ├── str_spec.rb │ │ ├── time_spec.rb │ │ ├── true_spec.rb │ │ ├── type_spec.rb │ │ ├── uri_spec.rb │ │ ├── uuid_v1_spec.rb │ │ ├── uuid_v2_spec.rb │ │ ├── uuid_v3_spec.rb │ │ ├── uuid_v4_spec.rb │ │ ├── uuid_v5_spec.rb │ │ ├── uuid_v6_spec.rb │ │ ├── uuid_v7_spec.rb │ │ └── uuid_v8_spec.rb │ ├── predicates_spec.rb │ ├── rule/ │ │ └── predicate_spec.rb │ ├── rule_compiler_spec.rb │ └── rule_spec.rb └── zizmor.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: hanami ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: "\U0001F41B Bug report" about: See CONTRIBUTING.md for more information title: '' labels: bug, help wanted assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. ## To Reproduce Provide detailed steps to reproduce, **an executable script would be best**. ## Expected behavior A clear and concise description of what you expected to happen. ## My environment - Ruby version: ... - OS: ... ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Community support url: https://discourse.hanamirb.org about: Please ask and answer questions here. ================================================ FILE: .github/SUPPORT.md ================================================ ## Support If you need help with any of the Hanami, Dry or Rom libraries, feel free to ask questions on our [discussion forum](https://discourse.hanamirb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: ================================================ FILE: .github/workflows/ci-lint.yml ================================================ name: CI lint on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] paths: - ".github/**" pull_request: branches: ["main", "release-*"] paths: - ".github/**" schedule: - cron: "0 0 * * 0" # every Sunday at midnight permissions: {} jobs: zizmor: name: Run zizmor runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Run zizmor uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d with: advanced-security: false ================================================ FILE: .github/workflows/ci.yml ================================================ # This file is synced from hanakai-rb/repo-sync name: CI run-name: ${{ github.ref_type == 'tag' && format('Release {0}', github.ref_name) || 'CI' }} on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] pull_request: branches: ["main", "release-*"] schedule: - cron: "30 4 * * *" permissions: contents: read jobs: tests: name: Tests (Ruby ${{ matrix.ruby }}) runs-on: ubuntu-latest continue-on-error: ${{ matrix.optional || false }} strategy: fail-fast: false matrix: ruby: - "4.0" - "3.4" - "3.3" - "jruby" include: - ruby: "4.0" coverage: "true" env: COVERAGE: ${{ matrix.coverage }} steps: - name: Checkout uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install package dependencies run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" - name: Set up Ruby uses: ruby/setup-ruby@0cb964fd540e0a24c900370abf38a33466142735 # zizmor: ignore[cache-poisoning] with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run all tests id: test run: | status=0 bundle exec rake || status=$? if [ ${status} -ne 0 ] && [ "${{ matrix.optional }}" == "true" ]; then echo "::warning::Optional matrix job failed." echo "optional_fail=true" >> "${GITHUB_OUTPUT}" echo "optional_fail_status=${status}" >> "${GITHUB_OUTPUT}" exit 0 # Ignore error here to keep the green checkmark fi exit ${status} - name: Create optional failure comment if: ${{ matrix.optional && github.event.pull_request }} uses: hanakai-rb/repo-sync/pr-comment-artifact@main with: name: ci-ruby-${{ matrix.ruby }} pr-number: ${{ github.event.pull_request.number }} comment-tag: ruby-${{ matrix.ruby }}-optional-failure message: "ℹ️ Optional job failed: Ruby ${{ matrix.ruby }}" mode: ${{ steps.test.outputs.optional_fail == 'true' && 'upsert' || 'delete' }} workflow-keepalive: if: github.event_name == 'schedule' runs-on: ubuntu-latest permissions: actions: write steps: - uses: liskin/gh-workflow-keepalive@7a9194bad497f0b993708eeaf10fc0a2d726eb71 release: runs-on: ubuntu-latest if: github.ref_type == 'tag' needs: tests steps: - name: Trigger release workflow uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a with: github-token: ${{ secrets.RELEASE_MACHINE_DISPATCH_TOKEN }} script: | const tag = context.ref.replace("refs/tags/", ""); const repo = context.repo.owner + "/" + context.repo.repo; const tagMessage = await github.rest.git.getTag({ owner: context.repo.owner, repo: context.repo.repo, tag_sha: context.sha }).then(res => res.data.message).catch(() => ""); const announce = /(skip-announce|no-announce)/i.test(tagMessage) ? "false" : "true"; await github.rest.actions.createWorkflowDispatch({ owner: "hanakai-rb", repo: "release-machine", workflow_id: "release.yml", ref: "main", inputs: { repo, tag, announce } }); const workflowUrl = "https://github.com/hanakai-rb/release-machine/actions/workflows/release.yml"; await core.summary .addHeading("Release Triggered") .addRaw(`Triggered release workflow for ${tag}`) .addLink("View release workflow", workflowUrl) .write(); ================================================ FILE: .github/workflows/pr-comments.yml ================================================ # This file is synced from hanakai-rb/repo-sync # Downloads comment artifacts from completed workflows and posts them to PRs. This allows source # workflows to run with read-only permissions on fork PRs while still posting comments via this # privileged workflow that runs in the base repo context. # # Comment artifacts should be generated using the `hanakai-rb/repo-sync/pr-comment-artifact@main` # action. name: PR comments on: # zizmor: ignore[dangerous-triggers] workflow_run: workflows: ["CI"] types: - completed permissions: pull-requests: write jobs: post-comments: runs-on: ubuntu-latest permissions: pull-requests: write if: github.event.workflow_run.event == 'pull_request' steps: - name: Post comments uses: hanakai-rb/repo-sync/pr-comments-from-artifacts@main with: workflow-run-id: ${{ github.event.workflow_run.id }} ================================================ FILE: .github/workflows/repo-sync-preview.yml ================================================ name: Repo-sync preview on: # zizmor: ignore[dangerous-triggers] workflow_run: workflows: ["CI", "RuboCop", "CI lint"] types: [completed] branches: - "ci/repo-sync-preview-*" jobs: report: runs-on: ubuntu-latest permissions: {} if: > github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.fork == false steps: - name: Dispatch status to repo-sync uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a with: github-token: ${{ secrets.REPO_SYNC_DISPATCH_TOKEN }} script: | const { BRANCH, REPO, WORKFLOW, STATUS, RUN_URL } = process.env; await github.rest.actions.createWorkflowDispatch({ owner: "hanakai-rb", repo: "repo-sync", workflow_id: "aggregate-preview-status.yml", ref: "main", inputs: { pr_number: BRANCH.replace("ci/repo-sync-preview-", ""), repo_name: REPO, workflow_name: WORKFLOW, status: STATUS, run_url: RUN_URL } }); env: BRANCH: ${{ github.event.workflow_run.head_branch }} REPO: ${{ github.repository }} WORKFLOW: ${{ github.event.workflow_run.name }} STATUS: ${{ github.event.workflow_run.conclusion }} RUN_URL: ${{ github.event.workflow_run.html_url }} ================================================ FILE: .github/workflows/rubocop.yml ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync name: RuboCop on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] pull_request: branches: ["main", "release-*"] permissions: contents: read jobs: build: runs-on: ubuntu-latest env: BUNDLE_ONLY: tools steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Set up Ruby 4.0 uses: ruby/setup-ruby@0cb964fd540e0a24c900370abf38a33466142735 # zizmor: ignore[cache-poisoning] with: ruby-version: 4.0 bundler-cache: true - name: Run RuboCop run: bundle exec rubocop --parallel ================================================ FILE: .gitignore ================================================ .DS_Store coverage /.bundle vendor/bundle tmp/ pkg/ .idea/ Gemfile.lock /.rubocop-* ================================================ FILE: .rspec ================================================ --color --require spec_helper --order random --warnings ================================================ FILE: .rubocop.yml ================================================ # This file is synced from hanakai-rb/repo-sync inherit_from: - https://raw.githubusercontent.com/hanakai-rb/repo-sync/main/rubocop/rubocop.yml <% if File.exist?(".rubocop.local.yml") %> - .rubocop.local.yml <% end %> ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Break Versioning](https://www.taoensso.com/break-versioning). ## [Unreleased] ### Changed - Set minumum Ruby version to 3.2 (@alassek) ## [1.6.0] - 2025-01-04 ### Added - Support for more UUID formats (via #110) (@Ptico) ### Fixed - Fix NoMethodError when trying to get AST of a Builder's result. (via #107) (@estum) ### Changed - Set 3.1 as minimum ruby version (@flash-gordon) [Compare v1.5.0...v1.6.0](https://github.com/dry-rb/dry-logic/compare/v1.5.0...v1.6.0) ## [1.5.0] - 2022-11-24 ### Added - `uri_rfc3986?` predicate that uses a better regexp than `uri?` (see #94 for more details) (@hieuk09) ### Changed - Made `Predicates.respond_to?` compatible with `Object#respond_to?` (via #105) (@solnic) - Made `Predicates.eql?` compatible with `Object#eql?` (via #106) (@solnic) [Compare v1.4.0...v1.5.0](https://github.com/dry-rb/dry-logic/compare/v1.4.0...v1.5.0) ## [1.4.0] - 2022-11-04 ### Changed - Updated to dry-core 1.0 (@flash-gordon + @solnic) [Compare v1.3.0...v1.4.0](https://github.com/dry-rb/dry-logic/compare/v1.3.0...v1.4.0) ## [1.3.0] - 2022-10-15 ### Changed - Use zeitwerk for auto-loading (@solnic + @flash-gordon) [Compare v1.2.0...v1.3.0](https://github.com/dry-rb/dry-logic/compare/v1.2.0...v1.3.0) ## [1.2.0] - 2021-04-26 ### Added - Add predicate and operation builder DSL (@oleander) [Compare v1.1.1...v1.2.0](https://github.com/dry-rb/dry-logic/compare/v1.1.1...v1.2.0) ## [1.1.1] - 2021-04-14 ### Fixed - Fixed a crash under jruby caused by arg splatting in Binary operations (@flash-gordon) [Compare v1.1.0...v1.1.1](https://github.com/dry-rb/dry-logic/compare/v1.1.0...v1.1.1) ## [1.1.0] - 2020-12-26 ### Fixed - Nested `Check` operations no longer crash under MRI 3.0 (@oleander) ### Changed - Switched to equalizer from dry-core (@solnic) [Compare v1.0.8...v1.1.0](https://github.com/dry-rb/dry-logic/compare/v1.0.8...v1.1.0) ## [1.0.8] - 2020-09-28 ### Fixed - Better Ruby 3 support with fixed specialization for rules of negative arity (@flash-gordon) [Compare v1.0.7...v1.0.8](https://github.com/dry-rb/dry-logic/compare/v1.0.7...v1.0.8) ## [1.0.7] - 2020-08-13 ### Added - A new `uri?` predicate that you can use to verify `URI` strings, ie `uri?("https", "https://dry-rb.org")` (@nerburish) - New predicates: `uuid_v1?`, `uuid_v2?`, `uuid_v3?` and `uuid_v5?` (via #75) (@jamesbrauman) [Compare v1.0.6...v1.0.7](https://github.com/dry-rb/dry-logic/compare/v1.0.6...v1.0.7) ## [1.0.6] - 2020-02-10 ### Fixed - Made the regexp used by `uuid_v4?` more secure (@kml) [Compare v1.0.5...v1.0.6](https://github.com/dry-rb/dry-logic/compare/v1.0.5...v1.0.6) ## [1.0.5] - 2019-11-07 ### Fixed - Make `format?` tolerant to `nil` values. It already worked like that before, but starting Ruby 2.7 it would produce warnings. Now it won't. Don't rely on this behavior, it's only added to make tests pass in dry-schema. Use explicit type checks instead (@flash-gordon) [Compare v1.0.4...v1.0.5](https://github.com/dry-rb/dry-logic/compare/v1.0.4...v1.0.5) ## [1.0.4] - 2019-11-06 ### Fixed - Fix keyword warnings (@flash-gordon) [Compare v1.0.3...v1.0.4](https://github.com/dry-rb/dry-logic/compare/v1.0.3...v1.0.4) ## [1.0.3] - 2019-08-01 ### Added - `bytesize?` predicate (@bmalinconico) - `min_bytesize?` predicate (@bmalinconico) - `max_bytesize? predicate (@bmalinconico) ### Changed - Min ruby version was set to `>= 2.4.0` (@flash-gordon) [Compare v1.0.2...v1.0.3](https://github.com/dry-rb/dry-logic/compare/v1.0.2...v1.0.3) ## [1.0.2] - 2019-06-14 Re-pushed 1.0.1 after dry-schema 1.2.0 release. [Compare v1.0.1...v1.0.2](https://github.com/dry-rb/dry-logic/compare/v1.0.1...v1.0.2) ## [1.0.1] - 2019-06-04 This release was removed from rubygems because it broke dry-schema. ### Added - `uuid_v4?` predicate (radar) - `respond_to?` predicate (waiting-for-dev) [Compare v1.0.0...v1.0.1](https://github.com/dry-rb/dry-logic/compare/v1.0.0...v1.0.1) ## [1.0.0] - 2019-04-23 ### Changed - Version bump to `1.0.0` (flash-gordon) [Compare v0.6.1...v1.0.0](https://github.com/dry-rb/dry-logic/compare/v0.6.1...v1.0.0) ## [0.6.1] - 2019-04-18 ### Fixed - Fix a regression in dry-validation 0.x for argument-less predicates (flash-gordon) [Compare v0.6.0...v0.6.1](https://github.com/dry-rb/dry-logic/compare/v0.6.0...v0.6.1) ## [0.6.0] - 2019-04-04 ### Added - Generating hints can be disabled by building `Operations::And` with `hints: false` option set (solnic) ### Changed - `Rule` construction has been optimized so that currying and application is multiple-times faster (flash-gordon) [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-logic/compare/v0.5.0...v0.6.0) ## [0.5.0] - 2019-01-29 ### Added - `:nil?` predicate (`none?` is now an alias) (solnic) ### Fixed - `Operation::Key#ast` will now return a correct AST with non-Undefined inputs (solnic) [Compare v0.4.2...v0.5.0](https://github.com/dry-rb/dry-logic/compare/v0.4.2...v0.5.0) ## [0.4.2] - 2017-09-15 ### Added - New `:case?` predicate matches a value against the given object with `#===` (flash-gordon) - New `:is?` predicate checks objects identity (using `#equal?`) (flash-gordon) ### Fixed - A bug with using custom predicates within a standalone module in `dry-validation` (flash-gordon) [Compare v0.4.1...v0.4.2](https://github.com/dry-rb/dry-logic/compare/v0.4.1...v0.4.2) ## [0.4.1] - 2017-01-23 ### Fixed - Warnings on MRI 2.4.0 are gone (jtippett) ### Changed - Predicates simply reuse other predicate methods instead of referring to them via `#[]` (georgemillo) [Compare v0.4.0...v0.4.1](https://github.com/dry-rb/dry-logic/compare/v0.4.0...v0.4.1) ## [0.4.0] - 2016-09-21 This is a partial rewrite focused on internal clean up and major performance improvements. This is also the beginning of the work to make this library first-class rather than "just" a rule backend for dry-validation and dry-types. ### Added - `Rule#[]` which applies a rule and always returns `true` or `false` (solnic) - `Rule#bind` which returns a rule with its predicate bound to a given object (solnic) - `Rule#eval_args` which evaluates unbound-methods-args in the context of a given object (solnic) - `Logic.Rule` builder function (solnic) - Nice `#inspect` on rules and operation objects (solnic) ### Changed - [BRAEKING] New result API (solnic) - [BREAKING] `Predicate` is now `Rule::Predicate` (solnic) - [BREAKING] `Rule::Conjunction` is now `Operation::And` (solnic) - [BREAKING] `Rule::Disjunction` is now `Operation::Or` (solnic) - [BREAKING] `Rule::ExlusiveDisjunction` is now `Operation::Xor` (solnic) - [BREAKING] `Rule::Implication` is now `Operation::Implication` (solnic) - [BREAKING] `Rule::Set` is now `Operation::Set` (solnic) - [BREAKING] `Rule::Each` is now `Operation::Each` (solnic) - [BREAKING] `Rule.new` accepts a predicate function as its first arg now (solnic) - [BREAKING] `Rule#name` is now `Rule#id` (solnic) - `Rule#parameters` is public now (solnic) [Compare v0.3.0...v0.4.0](https://github.com/dry-rb/dry-logic/compare/v0.3.0...v0.4.0) ## [0.3.0] - 2016-07-01 ### Added - `:type?` predicate imported from dry-types (solnic) - `Rule#curry` interface (solnic) ### Changed - Predicates AST now includes information about args (names & possible values) (fran-worley + solnic) - Predicates raise errors when they are called with invalid arity (fran-worley + solnic) - Rules no longer evaluate input twice when building result objects (solnic) [Compare v0.2.3...v0.3.0](https://github.com/dry-rb/dry-logic/compare/v0.2.3...v0.3.0) ## [0.2.3] - 2016-05-11 ### Added - `not_eql?`, `includes?`, `excludes?` predicates (fran-worley) ### Changed - Renamed `inclusion?` to `included_in?` and deprecated `inclusion?` (fran-worley) - Renamed `exclusion?` to `excluded_from?` and deprecated `exclusion?` (fran-worley) [Compare v0.2.2...v0.2.3](https://github.com/dry-rb/dry-logic/compare/v0.2.2...v0.2.3) ## [0.2.2] - 2016-03-30 ### Added - `number?`, `odd?`, `even?` predicates (fran-worley) [Compare v0.2.1...v0.2.2](https://github.com/dry-rb/dry-logic/compare/v0.2.1...v0.2.2) ## [0.2.1] - 2016-03-20 ### Fixed - Result AST for `Rule::Each` correctly maps elements with eql inputs (solnic) [Compare v0.2.0...v0.2.1](https://github.com/dry-rb/dry-logic/compare/v0.2.0...v0.2.1) ## [0.2.0] - 2016-03-11 ### Changed - Entire AST has been redefined (solnic) [Compare v0.1.4...v0.2.0](https://github.com/dry-rb/dry-logic/compare/v0.1.4...v0.2.0) ## [0.1.4] - 2016-01-27 ### Added - Support for hash-names in `Check` and `Result` which can properly resolve input from nested results (solnic) [Compare v0.1.3...v0.1.4](https://github.com/dry-rb/dry-logic/compare/v0.1.3...v0.1.4) ## [0.1.3] - 2016-01-27 ### Added - Support for resolving input from `Rule::Result` (solnic) ### Changed - `Check` and `Result` carry original input(s) (solnic) [Compare v0.1.2...v0.1.3](https://github.com/dry-rb/dry-logic/compare/v0.1.2...v0.1.3) ## [0.1.2] - 2016-01-19 ### Fixed - `xor` returns wrapped results when used against another result-rule (solnic) [Compare v0.1.1...v0.1.2](https://github.com/dry-rb/dry-logic/compare/v0.1.1...v0.1.2) ## [0.1.1] - 2016-01-18 ### Added - `Rule::Attr` which can be applied to a data object with attr readers (SunnyMagadan) - `Rule::Result` which can be applied to a result object (solnic) - `true?` and `false?` predicates (solnic) [Compare v0.1.0...v0.1.1](https://github.com/dry-rb/dry-logic/compare/v0.1.0...v0.1.1) ## [0.1.0] - 2016-01-11 Code extracted from dry-validation 0.4.1 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We pledge to make our community welcoming, safe, and equitable for all. We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. ## Encouraged Behaviors While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 2. Engaging **kindly and honestly** with others. 3. Respecting **different viewpoints** and experiences. 4. **Taking responsibility** for our actions and contributions. 5. Gracefully giving and accepting **constructive feedback**. 6. Committing to **repairing harm** when it occurs. 7. Behaving in other ways that promote and sustain the **well-being of our community**. ## Restricted Behaviors We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 7. Behaving in other ways that **threaten the well-being** of our community. ### Other Restrictions 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. ## Reporting an Issue Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. When an incident does occur, it is important to report it promptly. To report a possible violation, email conduct@hanakai.org. Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. ## Addressing and Repairing Harm If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 1) Warning 1) Event: A violation involving a single incident or series of incidents. 2) Consequence: A private, written warning from the Community Moderators. 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. 2) Temporarily Limited Activities 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. 3) Temporary Suspension 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 4) Permanent Ban 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. 3) Repair: There is no possible repair in cases of this severity. This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). ================================================ FILE: CONTRIBUTING.md ================================================ # Issue guidelines ## Reporting bugs If you’ve found a bug, please report an issue and describe the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. ## Reporting feature requests Report a feature request **only after discussing it first on [our forum](https://discourse.hanamirb.org)** and having it accepted. Please provide a concise description of the feature. ## Reporting questions, support requests, ideas, concerns etc. **Please don’t.** Use [our forum](https://discourse.hanamirb.org) instead. # Pull request guidelines A pull request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. Other requirements: 1. Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 2. Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 3. Add API documentation if it's a new feature. 4. Update API documentation if it changes an existing feature. 5. Bonus points for sending a PR which updates user documentation in our [site repository](https://github.com/hanakai-rb/site). # Asking for help If these guidelines aren't helpful, and you're stuck, please post a message on [our forum](https://discourse.dry-rb.org) or [find us in chat](https://discord.gg/KFCxDmk3JQ). ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" eval_gemfile "Gemfile.devtools" gemspec group :tools do gem "benchmark-ips", platform: :mri # gem "hotch", platform: :mri end ================================================ FILE: Gemfile.devtools ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync gem "rake", ">= 12.3.3" group :test do gem "simplecov", require: false, platforms: :ruby gem "simplecov-cobertura", require: false, platforms: :ruby gem "rexml", require: false gem "warning" end group :tools do gem "rubocop" end ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2026 Hanakai team 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. ================================================ FILE: README.md ================================================ [actions]: https://github.com/dry-rb/dry-logic/actions [chat]: https://discord.gg/naQApPAsZB [forum]: https://discourse.hanamirb.org [rubygem]: https://rubygems.org/gems/dry-logic # dry-logic [![Gem Version](https://badge.fury.io/rb/dry-logic.svg)][rubygem] [![CI Status](https://github.com/dry-rb/dry-logic/workflows/CI/badge.svg)][actions] [![Forum](https://img.shields.io/badge/Forum-dc360f?logo=discourse&logoColor=white)][forum] [![Chat](https://img.shields.io/badge/Chat-717cf8?logo=discord&logoColor=white)][chat] ## Links - [User documentation](https://dry-rb.org/gems/dry-logic) - [API documentation](http://rubydoc.info/gems/dry-logic) - [Forum](https://discourse.dry-rb.org) ## License See `LICENSE` file. ================================================ FILE: Rakefile ================================================ #!/usr/bin/env rake # frozen_string_literal: true require "bundler/gem_tasks" $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib")) require "rspec/core" require "rspec/core/rake_task" task default: :spec desc "Run all specs in spec directory" RSpec::Core::RakeTask.new(:spec) ================================================ FILE: benchmarks/builder.rb ================================================ # frozen_string_literal: true require "benchmark/ips" require "dry/logic" require "dry/logic/builder" def regular user_present = Dry::Logic::Rule::Predicate.new(Dry::Logic::Predicates[:key?]).curry(:user) min_age = Dry::Logic::Rule::Predicate.new(Dry::Logic::Predicates[:gt?]).curry(18) has_min_age = Dry::Logic::Operations::Key.new(min_age, name: %i[user age]) user_present & has_min_age end def builder Dry::Logic::Builder.call do key?(:user) & key(name: %i[user age]) { gt?(18) } end end Benchmark.ips do |x| x.report("builder") do builder.(user: {age: 19}) builder.(user: {age: 18}) end x.report("regular") do regular.(user: {age: 18}) regular.(user: {age: 19}) end x.compare! end ================================================ FILE: benchmarks/rule_application.rb ================================================ # frozen_string_literal: true require_relative "setup" unless Dry::Logic::Rule.respond_to?(:build) Dry::Logic::Rule.singleton_class.alias_method(:build, :new) end predicates = Dry::Logic::Predicates type_check = Dry::Logic::Rule.build(predicates[:type?]).curry(Integer) int_check = Dry::Logic::Rule.build(predicates[:int?]) key_check = Dry::Logic::Rule.build(predicates[:key?]).curry(:user) with_user = { user: {} } without_user = {} comparison = Dry::Logic::Rule.build(predicates[:gteq?]).curry(18) Benchmark.ips do |x| x.report("type check - success") { type_check.(0) } x.report("type check - failure") { type_check.("0") } x.report("int check - success") { int_check.(0) } x.report("int check - failure") { int_check.("0") } x.report("key check - success") { key_check.(with_user) } x.report("key check - failure") { key_check.(without_user) } x.report("comparison - success") { comparison.(20) } x.report("comparison - failure") { comparison.(17) } x.compare! end ================================================ FILE: benchmarks/setup.rb ================================================ # frozen_string_literal: true require "benchmark/ips" require "hotch" ENV["HOTCH_VIEWER"] ||= "open" require "dry/logic" require "dry/logic/predicates" def profile(&block) Hotch(filter: "Dry", &block) end ================================================ FILE: bin/.gitkeep ================================================ ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "dry/logic" require "dry/logic/predicates" # rubocop:disable Style/MixinUsage include Dry::Logic # rubocop:enable Style/MixinUsage require "irb" IRB.start ================================================ FILE: dry-logic.gemspec ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml. lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "dry/logic/version" Gem::Specification.new do |spec| spec.name = "dry-logic" spec.authors = ["Hanakai team"] spec.email = ["info@hanakai.org"] spec.license = "MIT" spec.version = Dry::Logic::VERSION.dup spec.summary = "Predicate logic with rule composition" spec.description = spec.summary spec.homepage = "https://dry-rb.org/gems/dry-logic" spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-logic.gemspec", "lib/**/*"] spec.bindir = "exe" spec.executables = Dir["exe/*"].map { |f| File.basename(f) } spec.require_paths = ["lib"] spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"] spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-logic/blob/main/CHANGELOG.md" spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-logic" spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-logic/issues" spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami" spec.required_ruby_version = ">= 3.3" spec.add_runtime_dependency "bigdecimal" spec.add_runtime_dependency "concurrent-ruby", "~> 1.0" spec.add_runtime_dependency "dry-core", "~> 1.1" spec.add_runtime_dependency "zeitwerk", "~> 2.6" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" end ================================================ FILE: examples/basic.rb ================================================ # frozen_string_literal: true require "dry/logic" require "dry/logic/predicates" # rubocop:disable Style/MixinUsage include Dry::Logic # rubocop:enable Style/MixinUsage user_present = Rule::Predicate.build(Predicates[:key?]).curry(:user) has_min_age = Operations::Key.new(Rule::Predicate.build(Predicates[:gt?]).curry(18), name: %i[user age]) user_rule = user_present & has_min_age puts user_rule.(user: {age: 19}).success? puts user_rule.(user: {age: 18}).success? ================================================ FILE: lib/dry/logic/appliable.rb ================================================ # frozen_string_literal: true module Dry module Logic module Appliable def id options[:id] end def result options[:result] end def applied? !result.nil? end def success? result.equal?(true) end def failure? !success? end def to_ast if applied? && id [success? ? :success : :failure, [id, ast]] else ast end end end end end ================================================ FILE: lib/dry/logic/builder.rb ================================================ # frozen_string_literal: true require "singleton" require "delegate" module Dry module Logic module Builder IGNORED_OPERATIONS = %i[ Abstract Binary Unary ].freeze IGNORED_PREDICATES = [ :predicate ].freeze # Predicate and operation builder # # @block [Proc] # @return [Builder::Result] # @example Check if input is zero # is_zero = Dry::Logic::Builder.call do # negation { lt?(0) ^ gt?(0) } # end # # p is_zero.call(1) # => false # p is_zero.call(0) # => true # p is_zero.call(-1) # => false def call(&) Context.instance.call(&) end module_function :call alias_method :build, :call public :call, :build class Context include Dry::Logic include Singleton module Predicates include Logic::Predicates end # @see Builder#call def call(&) instance_eval(&) end # Defines custom predicate # # @name [Symbol] Name of predicate # @Context [Proc] def predicate(name, context = nil, &block) if singleton_class.method_defined?(name) singleton_class.undef_method(name) end predicate = Rule::Predicate.new(context || block) define_singleton_method(name) do |*args| predicate.curry(*args) end end # Defines methods for operations and predicates def initialize Operations.constants(false).each do |name| next if IGNORED_OPERATIONS.include?(name) operation = Operations.const_get(name) define_singleton_method(name.downcase) do |*args, **kwargs, &block| operation.new(*call(&block), *args, **kwargs) end end Predicates::Methods.instance_methods(false).each do |name| unless IGNORED_PREDICATES.include?(name) predicate(name, Predicates[name]) end end end end end end end ================================================ FILE: lib/dry/logic/evaluator.rb ================================================ # frozen_string_literal: true module Dry module Logic class Evaluator include Dry::Equalizer(:path) attr_reader :path class Set include Dry::Equalizer(:evaluators) attr_reader :evaluators def self.new(paths) super(paths.map { |path| Evaluator::Key.new(path) }) end def initialize(evaluators) @evaluators = evaluators end def call(input) evaluators.map { |evaluator| evaluator[input] } end alias_method :[], :call end class Key < Evaluator def call(input) path.reduce(input) { |a, e| a[e] } end alias_method :[], :call end class Attr < Evaluator def call(input) path.reduce(input) { |a, e| a.public_send(e) } end alias_method :[], :call end def initialize(path) @path = Array(path) end end end end ================================================ FILE: lib/dry/logic/operations/abstract.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Abstract include Core::Constants include ::Dry::Equalizer(:rules, :options) include Operators attr_reader :rules attr_reader :options def initialize(*rules, **options) @rules = rules @options = options end def id options[:id] end def curry(*args) new(rules.map { |rule| rule.curry(*args) }, **options) end def new(rules, **new_options) self.class.new(*rules, **options, **new_options) end def with(new_options) new(rules, **options, **new_options) end def to_ast ast end end end end end ================================================ FILE: lib/dry/logic/operations/and.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class And < Binary attr_reader :hints def initialize(*, **) super @hints = options.fetch(:hints, true) end def type :and end alias_method :operator, :type def call(input) left_result = left.(input) if left_result.success? right_result = right.(input) if right_result.success? Result::SUCCESS else Result.new(false, id) { right_result.ast(input) } end else Result.new(false, id) do left_ast = left_result.to_ast hints ? [type, [left_ast, [:hint, right.ast(input)]]] : left_ast end end end def [](input) left[input] && right[input] end end end end end ================================================ FILE: lib/dry/logic/operations/attr.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Attr < Key def self.evaluator(name) Evaluator::Attr.new(name) end def type :attr end end end end end ================================================ FILE: lib/dry/logic/operations/binary.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Binary < Abstract attr_reader :left attr_reader :right def initialize(left, right, **options) super @left = left @right = right end def ast(input = Undefined) [type, [left.ast(input), right.ast(input)]] end def to_s "#{left} #{operator.to_s.upcase} #{right}" end end end end end ================================================ FILE: lib/dry/logic/operations/check.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Check < Unary attr_reader :evaluator def self.new(rule, **options) if options[:evaluator] super else keys = options.fetch(:keys) evaluator = Evaluator::Set.new(keys) super(rule, **options, evaluator: evaluator) end end def initialize(*rules, **options) super @evaluator = options[:evaluator] end def type :check end def call(input) *head, tail = evaluator[input] result = rule.curry(*head).(tail) if result.success? Result::SUCCESS else Result.new(false, id) { [type, [options[:keys], result.to_ast]] } end end def [](input) rule[*evaluator[input].reverse] end def ast(input = Undefined) [type, [options[:keys], rule.ast(input)]] end end end end end ================================================ FILE: lib/dry/logic/operations/each.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Each < Unary def type :each end def call(input) results = input.map { |element| rule.(element) } success = results.all?(&:success?) Result.new(success, id) do failures = results .map .with_index { |result, idx| [:key, [idx, result.ast(input[idx])]] if result.failure? } .compact [:set, failures] end end def [](arr) arr.all? { |input| rule[input] } end end end end end ================================================ FILE: lib/dry/logic/operations/implication.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Implication < Binary def type :implication end def operator :then end def call(input) left_result = left.(input) if left_result.success? right_result = right.(input) Result.new(right_result.success?, id) { right_result.to_ast } else Result::SUCCESS end end def [](input) if left[input] right[input] else true end end end end end end ================================================ FILE: lib/dry/logic/operations/key.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Key < Unary attr_reader :evaluator attr_reader :path def self.new(rules, **options) if options[:evaluator] super else name = options.fetch(:name) eval = options.fetch(:evaluator, evaluator(name)) super(rules, **options, evaluator: eval, path: name) end end def self.evaluator(name) Evaluator::Key.new(name) end def initialize(*rules, **options) super @evaluator = options[:evaluator] @path = options[:path] end def type :key end def call(hash) input = evaluator[hash] result = rule.(input) if result.success? Result::SUCCESS else Result.new(false, path) { [type, [path, result.to_ast]] } end end def [](hash) rule[evaluator[hash]] end def ast(input = Undefined) if input.equal?(Undefined) || !input.is_a?(Hash) [type, [path, rule.ast]] else [type, [path, rule.ast(evaluator[input])]] end end def to_s "#{type}[#{path}](#{rule})" end end end end end ================================================ FILE: lib/dry/logic/operations/negation.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Negation < Unary def type :not end def call(input) Result.new(rule.(input).failure?, id) { ast(input) } end def [](input) !rule[input] end end end end end ================================================ FILE: lib/dry/logic/operations/or.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Or < Binary def type :or end alias_method :operator, :type def call(input) left_result = left.(input) if left_result.success? Result::SUCCESS else right_result = right.(input) if right_result.success? Result::SUCCESS else Result.new(false, id) { [:or, [left_result.to_ast, right_result.to_ast]] } end end end def [](input) left[input] || right[input] end end end end end ================================================ FILE: lib/dry/logic/operations/set.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Set < Abstract def type :set end def call(input) results = rules.map { |rule| rule.(input) } success = results.all?(&:success?) Result.new(success, id) do [type, results.select(&:failure?).map(&:to_ast)] end end def [](input) rules.map { |rule| rule[input] }.all? end def ast(input = Undefined) [type, rules.map { |rule| rule.ast(input) }] end def to_s "#{type}(#{rules.map(&:to_s).join(", ")})" end end end end end ================================================ FILE: lib/dry/logic/operations/unary.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Unary < Abstract attr_reader :rule def initialize(*rules, **options) super @rule = rules.first end def ast(input = Undefined) [type, rule.ast(input)] end def to_s "#{type}(#{rule})" end end end end end ================================================ FILE: lib/dry/logic/operations/xor.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operations class Xor < Binary def type :xor end alias_method :operator, :type def call(input) Result.new(self[input], id) { ast(input) } end def [](input) left[input] ^ right[input] end def ast(input = Undefined) [type, rules.map { |rule| rule.ast(input) }] end end end end end ================================================ FILE: lib/dry/logic/operators.rb ================================================ # frozen_string_literal: true module Dry module Logic module Operators def and(other) Operations::And.new(self, other) end alias_method :&, :and def or(other) Operations::Or.new(self, other) end alias_method :|, :or def xor(other) Operations::Xor.new(self, other) end alias_method :^, :xor def then(other) Operations::Implication.new(self, other) end alias_method :>, :then end end end ================================================ FILE: lib/dry/logic/predicates.rb ================================================ # frozen_string_literal: true require "dry/core/constants" require "bigdecimal" require "bigdecimal/util" require "date" require "uri" module Dry module Logic module Predicates include ::Dry::Core::Constants module Methods def self.uuid_format(version) ::Regexp.new(<<~FORMAT.chomp, ::Regexp::IGNORECASE) \\A[0-9A-F]{8}-[0-9A-F]{4}-#{version}[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\\z FORMAT end UUIDv1 = uuid_format(1) UUIDv2 = uuid_format(2) UUIDv3 = uuid_format(3) UUIDv4 = uuid_format(4) UUIDv5 = uuid_format(5) UUIDv6 = uuid_format(6) UUIDv7 = uuid_format(7) UUIDv8 = uuid_format(8) def [](name) method(name) end def type?(type, input) = input.is_a?(type) def nil?(input) = input.nil? alias_method :none?, :nil? def key?(name, input) = input.key?(name) def attr?(name, input) = input.respond_to?(name) def empty?(input) case input when ::String, ::Array, ::Hash then input.empty? when nil then true else false end end def filled?(input) = !empty?(input) def bool?(input) = input.equal?(true) || input.equal?(false) def date?(input) = input.is_a?(::Date) def date_time?(input) = input.is_a?(::DateTime) def time?(input) = input.is_a?(::Time) def number?(input) true if Float(input) rescue ::ArgumentError, ::TypeError false end def int?(input) = input.is_a?(::Integer) def float?(input) = input.is_a?(::Float) def decimal?(input) = input.is_a?(::BigDecimal) def str?(input) = input.is_a?(::String) def hash?(input) = input.is_a?(::Hash) def array?(input) = input.is_a?(::Array) def odd?(input) = input.odd? def even?(input) = input.even? def lt?(num, input) = input < num def gt?(num, input) = input > num def lteq?(num, input) = !gt?(num, input) def gteq?(num, input) = !lt?(num, input) def size?(size, input) case size when ::Integer then size.equal?(input.size) when ::Range, ::Array then size.include?(input.size) else raise ::ArgumentError, "+#{size}+ is not supported type for size? predicate." end end def min_size?(num, input) = input.size >= num def max_size?(num, input) = input.size <= num def bytesize?(size, input) case size when ::Integer then size.equal?(input.bytesize) when ::Range, ::Array then size.include?(input.bytesize) else raise ::ArgumentError, "+#{size}+ is not supported type for bytesize? predicate." end end def min_bytesize?(num, input) = input.bytesize >= num def max_bytesize?(num, input) = input.bytesize <= num def inclusion?(list, input) deprecated(:inclusion?, :included_in?) included_in?(list, input) end def exclusion?(list, input) deprecated(:exclusion?, :excluded_from?) excluded_from?(list, input) end def included_in?(list, input) = list.include?(input) def excluded_from?(list, input) = !list.include?(input) def includes?(value, input) if input.respond_to?(:include?) input.include?(value) else false end rescue ::TypeError false end def excludes?(value, input) = !includes?(value, input) # This overrides Object#eql? so we need to make it compatible def eql?(left, right = Undefined) return super(left) if right.equal?(Undefined) left.eql?(right) end def is?(left, right) = left.equal?(right) def not_eql?(left, right) = !left.eql?(right) def true?(value) = value.equal?(true) def false?(value) = value.equal?(false) def format?(regex, input) = !input.nil? && regex.match?(input) def case?(pattern, input) = pattern === input # rubocop:disable Style/CaseEquality def uuid_v1?(input) = format?(UUIDv1, input) def uuid_v2?(input) = format?(UUIDv2, input) def uuid_v3?(input) = format?(UUIDv3, input) def uuid_v4?(input) = format?(UUIDv4, input) def uuid_v5?(input) = format?(UUIDv5, input) def uuid_v6?(input) = format?(UUIDv6, input) def uuid_v7?(input) = format?(UUIDv7, input) def uuid_v8?(input) = format?(UUIDv8, input) if defined?(::URI::RFC2396_PARSER) def uri?(schemes, input) uri_format = ::URI::RFC2396_PARSER.make_regexp(schemes) format?(uri_format, input) end else def uri?(schemes, input) uri_format = ::URI::DEFAULT_PARSER.make_regexp(schemes) format?(uri_format, input) end end def uri_rfc3986?(input) = format?(::URI::RFC3986_Parser::RFC3986_URI, input) # This overrides Object#respond_to? so we need to make it compatible def respond_to?(method, input = Undefined) return super if input.equal?(Undefined) input.respond_to?(method) end def predicate(name, &) define_singleton_method(name, &) end def deprecated(name, in_favor_of) Core::Deprecations.warn( "#{name} predicate is deprecated and will " \ "be removed in the next major version\n" \ "Please use #{in_favor_of} predicate instead", tag: "dry-logic", uplevel: 3 ) end end extend Methods def self.included(other) super other.extend(Methods) end end # rubocop:enable Metrics/ModuleLength end end ================================================ FILE: lib/dry/logic/result.rb ================================================ # frozen_string_literal: true module Dry module Logic class Result SUCCESS = ::Class.new { def success? true end def failure? false end }.new.freeze attr_reader :success attr_reader :id attr_reader :serializer def initialize(success, id = nil, &block) @success = success @id = id @serializer = block end def success? success end def failure? !success? end def type success? ? :success : :failure end def ast(input = Undefined) serializer.(input) end def to_ast if id [type, [id, ast]] else ast end end def to_s visit(to_ast) end private def visit(ast) __send__(:"visit_#{ast[0]}", ast[1]) end def visit_predicate(node) name, args = node if args.empty? name.to_s else "#{name}(#{args.map(&:last).map(&:inspect).join(", ")})" end end def visit_and(node) left, right = node "#{visit(left)} AND #{visit(right)}" end def visit_or(node) left, right = node "#{visit(left)} OR #{visit(right)}" end def visit_xor(node) left, right = node "#{visit(left)} XOR #{visit(right)}" end def visit_not(node) "not(#{visit(node)})" end def visit_hint(node) visit(node) end end end end ================================================ FILE: lib/dry/logic/rule/interface.rb ================================================ # frozen_string_literal: true module Dry module Logic class Rule class Interface < ::Module SPLAT = ["*rest"].freeze attr_reader :arity attr_reader :curried def initialize(arity, curried) super() @arity = arity @curried = curried if !variable_arity? && curried > arity raise ArgumentError, "wrong number of arguments (#{curried} for #{arity})" end define_constructor if curried? if constant? define_constant_application else define_application end end def constant? = arity.zero? def variable_arity? = arity.negative? def curried? = !curried.zero? def unapplied if variable_arity? unapplied = arity.abs - 1 - curried if unapplied.negative? 0 else unapplied end else arity - curried end end def name if constant? "Constant" else arity_str = if variable_arity? "Variable#{arity.abs - 1}Arity" else "#{arity}Arity" end curried_str = if curried? "#{curried}Curried" else EMPTY_STRING end "#{arity_str}#{curried_str}" end end def define_constructor assignment = if curried.equal?(1) "@arg0 = @args[0]" else "#{curried_args.join(", ")} = @args" end module_eval(<<~RUBY, __FILE__, __LINE__ + 1) def initialize(*) # def initialize(*) super # super # #{assignment} # @arg0 = @args[0] end # end RUBY end def define_constant_application module_exec do def call(*) if @predicate[] Result::SUCCESS else Result.new(false, id) { ast } end end def [](*) @predicate[] end end end def define_application splat = variable_arity? ? SPLAT : EMPTY_ARRAY parameters = (unapplied_args + splat).join(", ") application = "@predicate[#{(curried_args + unapplied_args + splat).join(", ")}]" module_eval(<<~RUBY, __FILE__, __LINE__ + 1) def call(#{parameters}) # def call(input0, input1, *rest) if #{application} # if @predicate[@arg0, @arg1, input0, input1, *rest] Result::SUCCESS # ::Dry::Logic::Result::Success else # else Result.new(false, id) { ast(#{parameters}) } # ::Dry::Logic::Result.new(false, id) { ast(input0, input1, *rest) } end # end end # end # def [](#{parameters}) # def [](@arg0, @arg1, input0, input1, *rest) #{application} # @predicate[@arg0, @arg1, input0, input1, *rest] end # end RUBY end def curried_args @curried_args ||= ::Array.new(curried) { "@arg#{_1}" } end def unapplied_args @unapplied_args ||= ::Array.new(unapplied) { "input#{_1}" } end end end end end ================================================ FILE: lib/dry/logic/rule/predicate.rb ================================================ # frozen_string_literal: true module Dry module Logic class Rule class Predicate < Rule def self.specialize(arity, curried, base = Predicate) super end def type :predicate end def name predicate.name end def to_s if args.empty? name.to_s else "#{name}(#{args.map(&:inspect).join(", ")})" end end def ast(input = Undefined) [type, [name, args_with_names(input)]] end alias_method :to_ast, :ast end end end end ================================================ FILE: lib/dry/logic/rule.rb ================================================ # frozen_string_literal: true require "concurrent/map" module Dry module Logic def self.Rule(*args, **options, &block) if args.any? Rule.build(*args, **options) elsif block Rule.build(block, **options) end end class Rule include Core::Constants include ::Dry::Equalizer(:predicate, :options) include Operators attr_reader :predicate attr_reader :options attr_reader :args attr_reader :arity def self.interfaces @interfaces ||= ::Concurrent::Map.new end def self.specialize(arity, curried, base = Rule) base.interfaces.fetch_or_store([arity, curried]) do interface = Interface.new(arity, curried) klass = ::Class.new(base) { include interface } base.const_set("#{base.name.split("::").last}#{interface.name}", klass) klass end end def self.build(predicate, args: EMPTY_ARRAY, arity: predicate.arity, **options) specialize(arity, args.size).new(predicate, {args: args, arity: arity, **options}) end def initialize(predicate, options = EMPTY_HASH) @predicate = predicate @options = options @args = options[:args] || EMPTY_ARRAY @arity = options[:arity] || predicate.arity end def type :rule end def id options[:id] end def curry(*new_args) with(args: args + new_args) end def bind(object) if predicate.respond_to?(:bind) self.class.build(predicate.bind(object), **options) else self.class.build( -> *args { object.instance_exec(*args, &predicate) }, **options, arity: arity, parameters: parameters ) end end def eval_args(object) with(args: args.map { |arg| arg.is_a?(UnboundMethod) ? arg.bind(object).() : arg }) end def with(new_opts) self.class.build(predicate, **options, **new_opts) end def parameters options[:parameters] || predicate.parameters end def ast(input = Undefined) [:predicate, [id, args_with_names(input)]] end private def args_with_names(*input) parameters.map(&:last).zip(args + input) end end end end ================================================ FILE: lib/dry/logic/rule_compiler.rb ================================================ # frozen_string_literal: true module Dry module Logic class RuleCompiler attr_reader :predicates def initialize(predicates) @predicates = predicates end def call(ast) ast.map { |node| visit(node) } end def visit(node) name, nodes = node send(:"visit_#{name}", nodes) end def visit_check(node) keys, predicate = node Operations::Check.new(visit(predicate), keys: keys) end def visit_not(node) Operations::Negation.new(visit(node)) end def visit_key(node) name, predicate = node Operations::Key.new(visit(predicate), name: name) end def visit_attr(node) name, predicate = node Operations::Attr.new(visit(predicate), name: name) end def visit_set(node) Operations::Set.new(*call(node)) end def visit_each(node) Operations::Each.new(visit(node)) end def visit_predicate(node) name, params = node predicate = Rule::Predicate.build(predicates[name]) if params.size > 1 args = params.map(&:last).reject { |val| val == Undefined } predicate.curry(*args) else predicate end end def visit_and(node) left, right = node visit(left).and(visit(right)) end def visit_or(node) left, right = node visit(left).or(visit(right)) end def visit_xor(node) left, right = node visit(left).xor(visit(right)) end def visit_implication(node) left, right = node visit(left).then(visit(right)) end end end end ================================================ FILE: lib/dry/logic/version.rb ================================================ # frozen_string_literal: true module Dry module Logic VERSION = "1.6.0" end end ================================================ FILE: lib/dry/logic.rb ================================================ # frozen_string_literal: true require "zeitwerk" require "dry/core" module Dry module Logic include Dry::Core::Constants def self.loader @loader ||= Zeitwerk::Loader.new.tap do |loader| root = File.expand_path("..", __dir__) loader.tag = "dry-logic" loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-logic.rb") loader.push_dir(root) loader.ignore( "#{root}/dry-logic.rb", "#{root}/dry/logic/version.rb" ) end end loader.setup end end ================================================ FILE: lib/dry-logic.rb ================================================ # frozen_string_literal: true require "dry/logic" ================================================ FILE: repo-sync.yml ================================================ name: gem: dry-logic constant: Dry::Logic github_org: dry-rb gemspec: authors: ["Hanakai team"] email: ["info@hanakai.org"] summary: "Predicate logic with rule composition" homepage: "https://dry-rb.org/gems/dry-logic" development_dependencies: - bundler - rake - rspec runtime_dependencies: - [bigdecimal] - [concurrent-ruby, "~> 1.0"] - [dry-core, "~> 1.1"] - [zeitwerk, "~> 2.6"] ================================================ FILE: spec/integration/builder/operation_spec.rb ================================================ # frozen_string_literal: true require_relative "../../shared/operation" RSpec.describe "operations" do describe "nested" do let(:predicate) do Dry::Logic::Builder.call do check keys: [:person] do check keys: [:age] do gt?(50) & lt?(200) end end end end describe "success" do subject { predicate.call({person: {age: 100}}) } it { is_expected.to be_a_success } end describe "failure" do subject { predicate.call({person: {age: 10}}) } it { is_expected.not_to be_a_success } end end describe :check do describe "one path" do let(:expression) do lambda do |*| check keys: [:age] do gt?(50) end end end describe "success" do it_behaves_like "operation" do let(:input) { {age: 100} } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { {age: 10} } let(:output) { false } end end end describe "two paths" do let(:expression) do lambda do |*| check keys: %i[speed limit] do gt? end end end describe "success" do it_behaves_like "operation" do let(:input) { {speed: 50, limit: 100} } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { {speed: 100, limit: 50} } let(:output) { false } end end end end describe :implication do let(:expression) do lambda do |*| implication { [gt?(0), lt?(10)] } end end describe "[true => true]" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { true } end end describe "[true => false]" do it_behaves_like "operation" do let(:input) { 20 } let(:output) { false } end end describe "[false => true]" do it_behaves_like "operation" do let(:input) { -5 } let(:output) { true } end end end describe :key do let(:expression) do lambda do |*| key name: %i[user age] do gt?(10) end end end describe "success" do it_behaves_like "operation" do let(:input) { {user: {age: 20}} } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { {user: {age: 5}} } let(:output) { false } end end end describe :and do let(:expression) do lambda do |*| even?.and(int?) end end describe "success" do it_behaves_like "operation" do let(:input) { 2 } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { false } end end end describe :or do let(:expression) do lambda do |*| even?.or(odd?) end end describe "success" do it_behaves_like "operation" do let(:input) { 10 } let(:output) { true } end it_behaves_like "operation" do let(:input) { 9 } let(:output) { true } end end end describe :negation do let(:expression) do lambda do |*| negation { even? } end end describe "success" do it_behaves_like "operation" do let(:input) { 9 } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { 6 } let(:output) { false } end end end describe :set do let(:expression) do lambda do |*| set { [lt?(5), gt?(2)] } end end describe "success" do it_behaves_like "operation" do let(:input) { 3 } let(:output) { true } end end describe "success" do it_behaves_like "operation" do let(:input) { 7 } let(:output) { false } end end end describe :each do let(:expression) do lambda do |*| each { gt?(10) } end end describe "success" do it_behaves_like "operation" do let(:input) { [20, 30] } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { [10, 20, 30] } let(:output) { false } end end end describe :xor do let(:expression) do lambda do |*| even?.xor(gt?(4)) end end describe "success" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { true } end it_behaves_like "operation" do let(:input) { 2 } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { 6 } let(:output) { false } end end end # describe :attr do # let(:expression) do # lambda do |*| # attr name: :age do # gt?(50) # end # end # end # # let(:person) { Struct.new(:age) } # # describe "success" do # it_behaves_like "operation" do # let(:input) { person.new(100) } # let(:output) { true } # end # end # # describe "failure" do # it_behaves_like "operation" do # let(:input) { person.new(0) } # let(:output) { false } # end # end # end describe "operators" do describe :& do let(:expression) do lambda do |*| even? & int? end end describe "success" do it_behaves_like "operation" do let(:input) { 2 } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { false } end end end describe :^ do let(:expression) do lambda do |*| even? ^ gt?(4) end end describe "success" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { true } end it_behaves_like "operation" do let(:input) { 2 } let(:output) { true } end end describe "failure" do it_behaves_like "operation" do let(:input) { 6 } let(:output) { false } end end end describe :> do let(:expression) do lambda do |*| gt?(0) > lt?(10) end end describe "[true => true]" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { true } end end describe "[true => false]" do it_behaves_like "operation" do let(:input) { 20 } let(:output) { false } end end describe "[false => true]" do it_behaves_like "operation" do let(:input) { -5 } let(:output) { true } end end end describe :then do let(:expression) do lambda do |*| gt?(0).then(lt?(10)) end end describe "[true => true]" do it_behaves_like "operation" do let(:input) { 5 } let(:output) { true } end end describe "[true => false]" do it_behaves_like "operation" do let(:input) { 20 } let(:output) { false } end end describe "[false => true]" do it_behaves_like "operation" do let(:input) { -5 } let(:output) { true } end end end describe :| do let(:expression) do lambda do |*| even? | odd? end end describe "success" do it_behaves_like "operation" do let(:input) { 10 } let(:output) { true } end it_behaves_like "operation" do let(:input) { 9 } let(:output) { true } end end end end end ================================================ FILE: spec/integration/builder/predicate_spec.rb ================================================ # frozen_string_literal: true require_relative "../../shared/predicate" RSpec.describe "predicates" do let(:input) { described_class } describe :key? do let(:expression) { ->(*) { key?(:speed) } } describe "success" do let(:output) { true } describe({speed: 100}) do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe({age: 50}) do it_behaves_like "predicate" end end end describe :format? do let(:expression) { ->(*) { format?(/^(A|B)$/) } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { "A" } end it_behaves_like "predicate" do let(:input) { "B" } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { "C" } end it_behaves_like "predicate" do let(:input) { "D" } end end end describe :type? do let(:expression) { ->(*) { type?(String) } } describe "success" do let(:output) { true } describe "string" do it_behaves_like "predicate" do let(:input) { "string" } end end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { :symbol } end end end describe :nil? do let(:expression) { ->(*) { nil? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { nil } end end describe "failure" do let(:output) { false } describe :symbol do it_behaves_like "predicate" end end end describe :attr? do let(:expression) { ->(*) { attr?(:name) } } describe "success" do let(:output) { true } describe Struct.new(:name).new("John") do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe Struct.new(:age).new(50) do it_behaves_like "predicate" end end end describe :empty? do let(:expression) { ->(*) { empty? } } describe "success" do let(:output) { true } describe({}) do it_behaves_like "predicate" end describe String do it_behaves_like "predicate" do let(:input) { described_class.new } end end describe [] do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe({key: "value"}) do it_behaves_like "predicate" end describe "string" do it_behaves_like "predicate" end describe nil do it_behaves_like "predicate" end describe [1, 2] do it_behaves_like "predicate" end end end describe :filled? do let(:expression) { ->(*) { filled? } } describe "success" do let(:output) { true } describe({key: "value"}) do it_behaves_like "predicate" end describe "string" do it_behaves_like "predicate" end describe nil do it_behaves_like "predicate" end describe [1, 2] do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe({}) do it_behaves_like "predicate" end describe String do it_behaves_like "predicate" do let(:input) { described_class.new } end end describe [] do it_behaves_like "predicate" end end end describe :bool? do let(:expression) { ->(*) { bool? } } describe "success" do let(:output) { true } describe true do it_behaves_like "predicate" end describe false do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe :symbol do it_behaves_like "predicate" end describe 5 do it_behaves_like "predicate" end end end describe :date? do let(:expression) { ->(*) { date? } } describe "success" do let(:output) { true } describe Date.new do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe :symbol do it_behaves_like "predicate" end end end describe :date_time? do let(:expression) { ->(*) { date_time? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { DateTime.new } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { :symbol } end end end describe :time? do let(:expression) { ->(*) { time? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { Time.new } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { :symbol } end end end describe :number? do let(:expression) { ->(*) { number? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { -4 } end it_behaves_like "predicate" do let(:input) { 10.0 } end it_behaves_like "predicate" do let(:input) { 10 } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { "A-4" } end it_behaves_like "predicate" do let(:input) { "A10" } end it_behaves_like "predicate" do let(:input) { nil } end it_behaves_like "predicate" do let(:input) { :nope } end end end describe :int? do let(:expression) { ->(*) { int? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { 10 } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { 10.0 } end end end describe :float? do let(:expression) { ->(*) { float? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { 1.0 } end end describe "success" do let(:output) { false } it_behaves_like "predicate" do let(:input) { 1 } end end end describe :decimal? do let(:expression) { ->(*) { decimal? } } describe "success" do let(:output) { true } it_behaves_like "predicate" do let(:input) { BigDecimal(1) } end end describe "failure" do let(:output) { false } it_behaves_like "predicate" do let(:input) { 10 } end end end describe :str? do let(:expression) { ->(*) { str? } } describe "success" do let(:output) { true } describe String do it_behaves_like "predicate" do let(:input) { described_class.new } end end end describe "failure" do let(:output) { false } describe Array do it_behaves_like "predicate" do let(:input) { described_class.new } end end end end describe :hash? do let(:expression) { ->(*) { hash? } } describe "success" do let(:output) { true } describe Hash do it_behaves_like "predicate" do let(:input) { described_class.new } end end end describe "failure" do let(:output) { false } describe Array do it_behaves_like "predicate" do let(:input) { described_class.new } end end end end describe :array? do let(:expression) { ->(*) { array? } } describe "success" do let(:output) { true } describe Array do it_behaves_like "predicate" do let(:input) { described_class.new } end end end describe "failure" do let(:output) { false } describe Hash do it_behaves_like "predicate" do let(:input) { described_class.new } end end end end describe :even? do let(:expression) { ->(*) { even? } } describe "success" do let(:output) { true } describe 10 do it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do let(:output) { false } describe 5 do it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :odd? do let(:expression) { ->(*) { odd? } } describe "success" do let(:output) { true } describe 5 do it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do let(:output) { false } describe 10 do it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :lt? do let(:expression) { ->(*) { lt?(10) } } describe "success" do let(:output) { true } describe 5 do it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do let(:output) { false } describe 200 do it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :gt? do let(:expression) { ->(*) { gt?(10) } } describe "failure" do let(:output) { false } describe 5 do it_behaves_like "predicate" do let(:input) { described_class } end end end describe "success" do let(:output) { true } describe 200 do it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :gteq? do let(:expression) { ->(*) { gteq?(10) } } describe "success" do let(:output) { true } describe 10 do it_behaves_like "predicate" do let(:input) { described_class } end end describe 11 do it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do let(:output) { false } describe 9 do it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :lteq? do let(:expression) { ->(*) { lteq?(10) } } describe "success" do let(:output) { true } describe 9 do it_behaves_like "predicate" end end describe "failure" do let(:output) { false } describe 11 do it_behaves_like "predicate" end end end describe :size? do describe "success" do let(:output) { true } describe "Integer" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?(2) } } let(:input) { [1, 2] } end end describe "Range" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?(1..3) } } let(:input) { [1, 2] } end end describe "Array" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?([3]) } } let(:input) { [1, 2, 3] } end end end describe "failure" do let(:output) { false } describe "Integer" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?(2) } } let(:input) { [1] } end end describe "Range" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?(1..3) } } let(:input) { [1, 2, 3, 4] } end end describe "Array" do it_behaves_like "predicate" do let(:expression) { ->(*) { size?([3]) } } let(:input) { [1, 2] } end end end end describe :min_size? do let(:expression) { ->(*) { min_size?(2) } } describe "success" do describe [1, 2, 3] do let(:output) { true } it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do describe [1] do let(:output) { false } it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :max_size? do let(:expression) { ->(*) { max_size?(2) } } describe "success" do describe [1] do let(:output) { true } it_behaves_like "predicate" do let(:input) { described_class } end end end describe "failure" do describe [1, 2, 3] do let(:output) { false } it_behaves_like "predicate" do let(:input) { described_class } end end end end describe :bytesize? do describe "success" do let(:output) { true } describe "Integer" do let(:expression) { ->(*) { bytesize?(1) } } it_behaves_like "predicate" do let(:input) { "A" } end end describe "Range" do let(:expression) { ->(*) { bytesize?(2..3) } } it_behaves_like "predicate" do let(:input) { "AB" } end end describe "Array" do let(:expression) { ->(*) { bytesize?([2, 3]) } } it_behaves_like "predicate" do let(:input) { "AB" } end end end describe "failure" do let(:output) { false } describe "Integer" do let(:expression) { ->(*) { bytesize?(1) } } it_behaves_like "predicate" do let(:input) { "AB" } end end describe "Range" do let(:expression) { ->(*) { bytesize?(2..3) } } it_behaves_like "predicate" do let(:input) { "A" } end end describe "Array" do let(:expression) { ->(*) { bytesize?([2, 3]) } } it_behaves_like "predicate" do let(:input) { "A" } end end end end describe :min_bytesize? do let(:expression) { ->(*) { min_bytesize?(1) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "A" } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { "" } end end end describe :max_bytesize? do let(:expression) { ->(*) { max_bytesize?(1) } } describe "success" do it_behaves_like "predicate" do let(:output) { false } let(:input) { "AB" } end end describe "failure" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "" } end end end describe :included_in? do let(:expression) { ->(*) { included_in?([1, 2, 3]) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 1 } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { 4 } end end end describe :excluded_from? do let(:expression) { ->(*) { excluded_from?([1, 2, 3]) } } describe "success" do it_behaves_like "predicate" do let(:output) { false } let(:input) { 1 } end end describe "failure" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 4 } end end end describe :includes? do let(:expression) { ->(*) { includes?(1) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { [1, 2, 3] } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { [2, 3, 4] } end end end describe :excludes? do describe Array do let(:expression) { ->(*) { excludes?(1) } } describe "success" do it_behaves_like "predicate" do let(:output) { false } let(:input) { described_class.new([1, 2, 3]) } end end describe "failure" do it_behaves_like "predicate" do let(:output) { true } let(:input) { described_class.new([2, 3, 4]) } end end end describe String do let(:expression) { ->(*) { excludes?("A") } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { described_class.new("B") } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { described_class.new("A") } end end end end describe "compare methods" do describe :eql? do let(:expression) { ->(*) { eql?(10) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 10 } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { 20 } end end end describe :not_eql? do let(:expression) { ->(*) { not_eql?(10) } } describe "success" do it_behaves_like "predicate" do let(:output) { false } let(:input) { 10 } end end describe "failure" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 20 } end end end end describe :is? do describe "success" do it_behaves_like "predicate" do let(:expression) { ->(*) { is?(nil) } } let(:output) { true } let(:input) { nil } end end describe "failure" do it_behaves_like "predicate" do let(:expression) { ->(*) { is?(Class.new) } } let(:output) { false } let(:input) { Class.new } end end end describe "true? & false?" do describe :true? do let(:expression) { ->(*) { true? } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { true } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { false } end end end describe :false? do let(:expression) { ->(*) { false? } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { false } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { true } end end end end describe :case? do describe "Range" do let(:expression) { ->(*) { case?(5..10) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 6 } end end end describe "Fixnum" do let(:expression) { ->(*) { case?(Integer) } } describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { 10 } end end end end describe "uuid" do describe :uuid_v2? describe :uuid_v3? describe :uuid_v5? describe :uuid_v1? do let(:expression) do ->(*) { uuid_v1? } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "554ef240-5433-11eb-ae93-0242ac130002" } end end end describe :uuid_v4? do let(:expression) do ->(*) { uuid_v4? } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "f2711f00-5602-45c7-ae01-c94d285592c3" } end end end end describe :uri? do describe :http do let(:expression) do ->(*) { uri?(:http) } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "http://google.com" } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { "https://google.com" } end end end describe [:http, :https] do let(:expression) do ->(*) { uri?(%w[http https]) } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "https://google.com" } end it_behaves_like "predicate" do let(:output) { true } let(:input) { "http://google.com" } end end end describe Regexp do let(:expression) do ->(*) { uri?(/https?/) } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "https://google.com" } end it_behaves_like "predicate" do let(:output) { true } let(:input) { "http://google.com" } end end end describe :https do let(:expression) do ->(*) { uri?(:https) } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { "https://google.com" } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { "http://google.com" } end end end end describe :respond_to? do let(:expression) do ->(*) { respond_to?(:awesome?) } end describe "success" do it_behaves_like "predicate" do let(:output) { true } let(:input) { Struct.new(:awesome?).new(true) } end end describe "failure" do it_behaves_like "predicate" do let(:output) { false } let(:input) { Struct.new(:not_awesome?).new(true) } end end end describe :predicate do before { extend Dry::Logic::Builder } before(:each) do build do predicate :divisible_with? do |num, input| (input % num).zero? end end end let(:expression) do lambda do |*| divisible_with?(10) end end describe "success" do it_behaves_like "predicate" do let(:input) { 10 } let(:output) { true } end end describe "success" do it_behaves_like "predicate" do let(:input) { 3 } let(:output) { false } end end end end ================================================ FILE: spec/integration/result_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Result do include_context "predicates" describe "#to_s" do shared_examples_for "string representation" do it "returns string representation" do expect(rule.(input).to_s).to eql(output) end end context "with a predicate" do let(:rule) { Dry::Logic::Rule::Predicate.build(gt?, args: [18]) } let(:input) { 17 } let(:output) { "gt?(18, 17)" } it_behaves_like "string representation" end context "with AND operation" do let(:rule) do Dry::Logic::Rule::Predicate.build(array?).and(Dry::Logic::Rule::Predicate.build(empty?)) end let(:input) { "" } let(:output) { 'array?("") AND empty?("")' } it_behaves_like "string representation" end context "with OR operation" do let(:rule) do Dry::Logic::Rule::Predicate.build(array?).or(Dry::Logic::Rule::Predicate.build(empty?)) end let(:input) { 123 } let(:output) { "array?(123) OR empty?(123)" } it_behaves_like "string representation" end context "with XOR operation" do let(:rule) do Dry::Logic::Rule::Predicate.build(array?).xor(Dry::Logic::Rule::Predicate.build(empty?)) end let(:input) { [] } let(:output) { "array?([]) XOR empty?([])" } it_behaves_like "string representation" end context "with THEN operation" do let(:rule) do Dry::Logic::Rule::Predicate.build(array?).then(Dry::Logic::Rule::Predicate.build(empty?)) end let(:input) { [1, 2, 3] } let(:output) { "empty?([1, 2, 3])" } it_behaves_like "string representation" end context "with NOT operation" do let(:rule) { Dry::Logic::Operations::Negation.new(Dry::Logic::Rule::Predicate.build(array?)) } let(:input) { "foo" } let(:output) { 'not(array?("foo"))' } it_behaves_like "string representation" end end end ================================================ FILE: spec/integration/rule_spec.rb ================================================ # frozen_string_literal: true require "dry-logic" RSpec.describe "Rules" do specify "defining an anonymous rule with an arbitrary predicate" do rule = Dry::Logic.Rule { |value| value.is_a?(Integer) } expect(rule.(1)).to be_success expect(rule[1]).to be(true) end specify "defining a conjunction" do rule = Dry::Logic.Rule(&:even?) & Dry::Logic.Rule { |v| v > 4 } expect(rule.(3)).to be_failure expect(rule.(4)).to be_failure expect(rule.(5)).to be_failure expect(rule.(6)).to be_success end specify "defining a disjunction" do rule = Dry::Logic.Rule { |v| v < 4 } | Dry::Logic.Rule { |v| v > 6 } expect(rule.(5)).to be_failure expect(rule.(3)).to be_success expect(rule.(7)).to be_success end specify "defining an implication" do rule = Dry::Logic.Rule(&:empty?) > Dry::Logic.Rule { |v| v.is_a?(Array) } expect(rule.("foo")).to be_success expect(rule.([1, 2])).to be_success expect(rule.([])).to be_success expect(rule.("")).to be_failure end specify "defining an exclusive disjunction" do rule = Dry::Logic.Rule(&:empty?) ^ Dry::Logic.Rule { |v| v.is_a?(Array) } expect(rule.("foo")).to be_failure expect(rule.([])).to be_failure expect(rule.([1, 2])).to be_success expect(rule.("")).to be_success end specify "defining a rule with options" do # using &:empty? breaks the spec rule = Dry::Logic::Rule(id: :empty?) { |value| value.empty? } # rubocop:enable Style/SymbolProc expect(rule.("foo")).to be_failure expect(rule.("")).to be_success expect(rule.ast("foo")).to eql([:predicate, [:empty?, [[:value, "foo"]]]]) end end ================================================ FILE: spec/shared/built_rule.rb ================================================ # frozen_string_literal: true require "dry/logic/builder" RSpec.shared_examples "built rule" do |rule_type, *args| subject(:built_rule) { Dry::Logic::Builder.call(&expression) } let(:rule_type) { rule_type } case rule_type when :predicate include_examples "built rule predicate", *args else include_examples "built rule operation", *args end describe "#to_ast" do subject(:ast) { built_rule.to_ast } it { is_expected.to include(rule_type, rule_node) } end end RSpec.shared_examples "built rule predicate" do |predicate_name, *args| it { is_expected.to be_kind_of(Dry::Logic::Rule::Predicate) } let(:predicate_name) { predicate_name } let(:rule_node) { [predicate_name, include(*args)] } describe "#predicate" do subject(:predicate) { built_rule.predicate } describe "#name" do subject(:name) { predicate.name } it { is_expected.to eq(predicate_name) } end end end RSpec.shared_examples "built rule operation" do |node| it { is_expected.to be_kind_of(Dry::Logic::Operation::Abstract) } let(:rule_node) { node } end ================================================ FILE: spec/shared/operation.rb ================================================ # frozen_string_literal: true require "dry/logic/builder" RSpec.shared_examples "operation" do before { extend Dry::Logic::Builder } let(:operation) { build(&expression) } let(:args) { defined?(input) ? [input] : [] } subject { operation.call(*args).success? } it { is_expected.to eq(output) } end ================================================ FILE: spec/shared/predicate.rb ================================================ # frozen_string_literal: true require "dry/logic/builder" # TODO: Merge with {operation}? RSpec.shared_examples "predicate" do before { extend Dry::Logic::Builder } let(:predicate) { build(&expression) } let(:args) { defined?(input) ? [input] : [] } subject { predicate.call(*args).success? } it { is_expected.to eq(output) } end ================================================ FILE: spec/shared/predicates.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.shared_examples "predicates" do let(:nil?) { Dry::Logic::Predicates[:nil?] } let(:array?) { Dry::Logic::Predicates[:array?] } let(:empty?) { Dry::Logic::Predicates[:empty?] } let(:str?) { Dry::Logic::Predicates[:str?] } let(:true?) { Dry::Logic::Predicates[:true?] } let(:hash?) { Dry::Logic::Predicates[:hash?] } let(:int?) { Dry::Logic::Predicates[:int?] } let(:filled?) { Dry::Logic::Predicates[:filled?] } let(:min_size?) { Dry::Logic::Predicates[:min_size?] } let(:lt?) { Dry::Logic::Predicates[:lt?] } let(:gt?) { Dry::Logic::Predicates[:gt?] } let(:key?) { Dry::Logic::Predicates[:key?] } let(:attr?) { Dry::Logic::Predicates[:attr?] } let(:eql?) { Dry::Logic::Predicates[:eql?] } let(:size?) { Dry::Logic::Predicates[:size?] } let(:case?) { Dry::Logic::Predicates[:case?] } let(:equal?) { Dry::Logic::Predicates[:equal?] } end RSpec.shared_examples "a passing predicate" do let(:predicate) { Dry::Logic::Predicates[predicate_name] } it do arguments_list.each do |args| expect(predicate.call(*args)).to be(true) end end end RSpec.shared_examples "a failing predicate" do let(:predicate) { Dry::Logic::Predicates[predicate_name] } it do arguments_list.each do |args| expect(predicate.call(*args)).to be(false) end end end ================================================ FILE: spec/shared/rule.rb ================================================ # frozen_string_literal: true RSpec.shared_examples_for Dry::Logic::Rule do let(:arity) { 2 } let(:predicate) { double(:predicate, arity: arity, name: predicate_name) } let(:rule_type) { described_class } let(:predicate_name) { :good? } describe "#arity" do it "returns its predicate arity" do rule = rule_type.build(predicate) expect(rule.arity).to be(2) end end describe "#parameters" do it "returns a list of args with their names" do rule = rule_type.build(-> foo, bar { true }, args: [312]) expect(rule.parameters).to eql([%i[req foo], %i[req bar]]) end end describe "#call" do let(:arity) { 1 } it "returns success for valid input" do rule = rule_type.build(predicate) expect(predicate).to receive(:[]).with(2).and_return(true) expect(rule.(2)).to be_success end it "returns failure for invalid input" do rule = rule_type.build(predicate) expect(predicate).to receive(:[]).with(2).and_return(false) expect(rule.(2)).to be_failure end end describe "#[]" do let(:arity) { 1 } it "delegates to its predicate" do rule = rule_type.build(predicate) expect(predicate).to receive(:[]).with(2).and_return(true) expect(rule[2]).to be(true) end end describe "#curry" do it "returns a curried rule" do rule = rule_type.build(predicate).curry(3) expect(predicate).to receive(:[]).with(3, 2).and_return(true) expect(rule.args).to eql([3]) expect(rule.(2)).to be_success end it "raises argument error when arity does not match" do expect(predicate).to receive(:arity).and_return(2) expect { rule_type.build(predicate).curry(3, 2, 1) }.to raise_error( ArgumentError, "wrong number of arguments (3 for 2)" ) end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require_relative "support/coverage" require_relative "support/warnings" begin require "pry-byebug" rescue LoadError; end require "dry/logic" require "pathname" SPEC_ROOT = Pathname(__dir__) Dir[SPEC_ROOT.join("shared/**/*.rb")].each(&method(:require)) Dir[SPEC_ROOT.join("support/**/*.rb")].each(&method(:require)) RSpec.configure do |config| config.include Module.new { def undefined Dry::Core::Constants::Undefined end } end ================================================ FILE: spec/support/coverage.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync if ENV["COVERAGE"] == "true" require "simplecov" require "simplecov-cobertura" SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter SimpleCov.start do add_filter "/spec/" enable_coverage :branch end end ================================================ FILE: spec/support/mutant.rb ================================================ # frozen_string_literal: true module Mutant class Selector class Expression < self def call(_subject) integration.all_tests end end end end ================================================ FILE: spec/support/rspec.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync RSpec.configure do |config| # When no filter given, search and run focused tests config.filter_run_when_matching :focus # Disables rspec monkey patches (no reason for their existence tbh) config.disable_monkey_patching! # Run ruby in verbose mode config.warnings = true # Collect all failing expectations automatically, # without calling aggregate_failures everywhere config.define_derived_metadata do |meta| meta[:aggregate_failures] = true end if ENV["CI"] # No focused specs should be committed. This ensures # builds fail when this happens. config.before(:each, :focus) do raise StandardError, "You've committed a focused spec!" end end end ================================================ FILE: spec/support/warnings.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync require "warning" # Ignore warnings for experimental features Warning[:experimental] = false if Warning.respond_to?(:[]) # Ignore all warnings coming from gem dependencies Gem.path.each do |path| Warning.ignore(//, path) end ================================================ FILE: spec/unit/builder_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Builder do describe "undefined methods" do it "raises NameError" do expect do described_class.call { does_not_exist } end.to raise_error(NameError, /does_not_exist/) end end describe "leakage" do context "given a module extending ::Builder" do subject do Module.new do extend Dry::Logic::Builder end end it { is_expected.not_to respond_to(:int?) } it { is_expected.to respond_to(:call) } it { is_expected.to respond_to(:build) } end end describe "ast of built rule" do let(:expression) { -> (*) { key?(:speed) } } it_behaves_like "built rule", :predicate, :key?, [:name, :speed] end end ================================================ FILE: spec/unit/operations/and_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::And do subject(:operation) { described_class.new(left, right) } include_context "predicates" let(:left) { Dry::Logic::Rule::Predicate.build(int?) } let(:right) { Dry::Logic::Rule::Predicate.build(gt?).curry(18) } describe "#call" do it "calls left and right" do expect(operation.(18)).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:and, [[:predicate, [:int?, [[:input, undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, undefined]]]]]] ) end it "returns result ast" do expect(operation.("18").to_ast).to eql( [:and, [[:predicate, [:int?, [[:input, "18"]]]], [:hint, [:predicate, [:gt?, [[:num, 18], [:input, "18"]]]]]]] ) expect(operation.with(hints: false).("18").to_ast).to eql( [:predicate, [:int?, [[:input, "18"]]]] ) expect(operation.(18).to_ast).to eql( [:predicate, [:gt?, [[:num, 18], [:input, 18]]]] ) end it "returns failure result ast" do expect(operation.with(id: :age).("18").to_ast).to eql( [:failure, [:age, [:and, [[:predicate, [:int?, [[:input, "18"]]]], [:hint, [:predicate, [:gt?, [[:num, 18], [:input, "18"]]]]]]]]] ) expect(operation.with(id: :age).(18).to_ast).to eql( [:failure, [:age, [:predicate, [:gt?, [[:num, 18], [:input, 18]]]]]] ) end end describe "#and" do let(:other) { Dry::Logic::Rule::Predicate.build(lt?).curry(30) } it "creates and with the other" do expect(operation.and(other).(31)).to be_failure end end describe "#or" do let(:other) { Dry::Logic::Rule::Predicate.build(lt?).curry(14) } it "creates or with the other" do expect(operation.or(other).(13)).to be_success end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("int? AND gt?(18)") end end end ================================================ FILE: spec/unit/operations/attr_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Attr do subject(:operation) do Dry::Logic::Operations::Attr.new(Dry::Logic::Rule::Predicate.build(str?), name: :name) end include_context "predicates" let(:model) { Struct.new(:name) } describe "#call" do it "applies predicate to the value" do expect(operation.(model.new("Jane"))).to be_success expect(operation.(model.new(nil))).to be_failure end end describe "#and" do let(:other) do Dry::Logic::Operations::Attr.new(Dry::Logic::Rule::Predicate.build(min_size?).curry(3), name: :name) end it "returns and where value is passed to the right" do present_and_string = operation.and(other) expect(present_and_string.(model.new("Jane"))).to be_success expect(present_and_string.(model.new("Ja"))).to be_failure expect(present_and_string.(model.new(1))).to be_failure end end end ================================================ FILE: spec/unit/operations/check_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Check do include_context "predicates" describe "#call" do context "with 1-level nesting" do subject(:operation) do described_class.new(Dry::Logic::Rule::Predicate.build(eql?).curry(1), id: :compare, keys: [:num]) end it "applies predicate to args extracted from the input" do expect(operation.(num: 1)).to be_success expect(operation.(num: 2)).to be_failure end end context "with 2-levels nesting" do subject(:operation) do described_class.new( Dry::Logic::Rule::Predicate.build(eql?), id: :compare, keys: [[:nums, :left], [:nums, :right]] ) end it "applies predicate to args extracted from the input" do expect(operation.(nums: {left: 1, right: 1})).to be_success expect(operation.(nums: {left: 1, right: 2})).to be_failure end it "curries args properly" do result = operation.(nums: {left: 1, right: 2}) expect(result.to_ast).to eql( [:failure, [:compare, [:check, [ [[:nums, :left], [:nums, :right]], [:predicate, [:eql?, [[:left, 1], [:right, 2]]]] ]]]] ) end end context "with its output as input" do let(:gt?) { Dry::Logic::Predicates[:gt?] } let(:min) { Dry::Logic::Rule::Predicate.new(gt?).curry(18) } let(:inner) { described_class.new(min, keys: [:age]) } let(:outer) { described_class.new(inner, keys: [:person]) } subject { outer.call(input) } describe "success" do let(:input) { {person: {age: 20}} } it { is_expected.to be_a_success } end describe "failure" do let(:input) { {person: {age: 10}} } it { is_expected.not_to be_a_success } end end end describe "#to_ast" do subject(:operation) do described_class.new(Dry::Logic::Rule::Predicate.build(str?), keys: [:email]) end it "returns ast" do expect(operation.to_ast).to eql( [:check, [[:email], [:predicate, [:str?, [[:input, undefined]]]]]] ) end end end ================================================ FILE: spec/unit/operations/each_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Each do subject(:operation) { described_class.new(is_string) } include_context "predicates" let(:is_string) { Dry::Logic::Rule::Predicate.build(str?) } describe "#call" do it "applies its rules to all elements in the input" do expect(operation.(["Address"])).to be_success expect(operation.([nil, "Address"])).to be_failure expect(operation.([:Address, "Address"])).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql([:each, [:predicate, [:str?, [[:input, undefined]]]]]) end it "returns result ast" do expect(operation.([nil, 12, nil]).to_ast).to eql( [:set, [ [:key, [0, [:predicate, [:str?, [[:input, nil]]]]]], [:key, [1, [:predicate, [:str?, [[:input, 12]]]]]], [:key, [2, [:predicate, [:str?, [[:input, nil]]]]]] ]] ) end it "returns failure result ast" do expect(operation.with(id: :tags).([nil, "red", 12]).to_ast).to eql( [:failure, [:tags, [:set, [ [:key, [0, [:predicate, [:str?, [[:input, nil]]]]]], [:key, [2, [:predicate, [:str?, [[:input, 12]]]]]] ]]]] ) end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("each(str?)") end end end ================================================ FILE: spec/unit/operations/implication_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Implication do subject(:operation) { described_class.new(left, right) } include_context "predicates" let(:left) { Dry::Logic::Rule::Predicate.build(int?) } let(:right) { Dry::Logic::Rule::Predicate.build(gt?).curry(18) } describe "#call" do it "calls left and right" do expect(operation.("19")).to be_success expect(operation.(19)).to be_success expect(operation.(18)).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:implication, [[:predicate, [:int?, [[:input, undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, undefined]]]]]] ) end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("int? THEN gt?(18)") end end end ================================================ FILE: spec/unit/operations/key_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Key do subject(:operation) { described_class.new(predicate, name: :user) } include_context "predicates" let(:predicate) do Dry::Logic::Rule::Predicate.build(key?).curry(:age) end describe "#call" do context "with a plain predicate" do it "returns a success for valid input" do expect(operation.(user: {age: 18})).to be_success end it "returns a failure for invalid input" do result = operation.(user: {}) expect(result).to be_failure expect(result.to_ast).to eql( [:failure, [:user, [:key, [:user, [:predicate, [:key?, [[:name, :age], [:input, {}]]]]]]]] ) end end context "with a set rule as predicate" do subject(:operation) do described_class.new(predicate, name: :address) end let(:predicate) do Dry::Logic::Operations::Set.new( Dry::Logic::Rule::Predicate.build(key?).curry(:city), Dry::Logic::Rule::Predicate.build(key?).curry(:zipcode) ) end it "applies set rule to the value that passes" do result = operation.(address: {city: "NYC", zipcode: "123"}) expect(result).to be_success end it "applies set rule to the value that fails" do result = operation.(address: {city: "NYC"}) expect(result).to be_failure expect(result.to_ast).to eql( [:failure, [:address, [:key, [:address, [:set, [ [:predicate, [:key?, [[:name, :zipcode], [:input, {city: "NYC"}]]]] ]]]]]] ) end end context "with an each rule as predicate" do subject(:operation) do described_class.new(predicate, name: :nums) end let(:predicate) do Dry::Logic::Operations::Each.new(Dry::Logic::Rule::Predicate.build(str?)) end it "applies each rule to the value that passses" do result = operation.(nums: %w[1 2 3]) expect(result).to be_success end it "applies each rule to the value that fails" do failure = operation.(nums: [1, "3", 3]) expect(failure).to be_failure expect(failure.to_ast).to eql( [:failure, [:nums, [:key, [:nums, [:set, [ [:key, [0, [:predicate, [:str?, [[:input, 1]]]]]], [:key, [2, [:predicate, [:str?, [[:input, 3]]]]]] ]]]]]] ) end end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:key, [:user, [:predicate, [:key?, [[:name, :age], [:input, undefined]]]]]] ) end end describe "#ast" do it "returns ast without the input" do expect(operation.ast).to eql( [:key, [:user, [:predicate, [:key?, [[:name, :age], [:input, undefined]]]]]] ) end it "returns ast with the input" do expect(operation.ast(user: "jane")).to eql( [:key, [:user, [:predicate, [:key?, [[:name, :age], [:input, "jane"]]]]]] ) end end describe "#and" do subject(:operation) do described_class.new(Dry::Logic::Rule::Predicate.build(str?), name: [:user, :name]) end let(:other) do described_class.new(Dry::Logic::Rule::Predicate.build(filled?), name: [:user, :name]) end it "returns and rule where value is passed to the right" do present_and_string = operation.and(other) expect(present_and_string.(user: {name: "Jane"})).to be_success expect(present_and_string.(user: {})).to be_failure expect(present_and_string.(user: {name: 1})).to be_failure end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("key[user](key?(:age))") end end end ================================================ FILE: spec/unit/operations/negation_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Negation do subject(:operation) { described_class.new(is_int) } include_context "predicates" let(:is_int) { Dry::Logic::Rule::Predicate.build(int?) } describe "#call" do it "negates its rule" do expect(operation.("19")).to be_success expect(operation.(17)).to be_failure end context "double negation" do subject(:double_negation) { described_class.new(operation) } it "works as rule" do expect(double_negation.("19")).to be_failure expect(double_negation.(17)).to be_success end end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:not, [:predicate, [:int?, [[:input, undefined]]]]] ) end it "returns result ast" do expect(operation.(17).to_ast).to eql( [:not, [:predicate, [:int?, [[:input, 17]]]]] ) end it "returns result ast with an :id" do expect(operation.with(id: :age).(17).to_ast).to eql( [:failure, [:age, [:not, [:predicate, [:int?, [[:input, 17]]]]]]] ) end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("not(int?)") end end end ================================================ FILE: spec/unit/operations/or_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Or do subject(:operation) { described_class.new(left, right) } include_context "predicates" let(:left) { Dry::Logic::Rule::Predicate.build(nil?) } let(:right) { Dry::Logic::Rule::Predicate.build(gt?).curry(18) } let(:other) do Dry::Logic::Rule::Predicate.build(int?) & Dry::Logic::Rule::Predicate.build(lt?).curry(14) end describe "#call" do it "calls left and right" do expect(operation.(nil)).to be_success expect(operation.(19)).to be_success expect(operation.(18)).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:or, [ [:predicate, [:nil?, [[:input, undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, undefined]]]] ]] ) end it "returns result ast" do expect(operation.(17).to_ast).to eql( [:or, [ [:predicate, [:nil?, [[:input, 17]]]], [:predicate, [:gt?, [[:num, 18], [:input, 17]]]] ]] ) end it "returns failure result ast" do expect(operation.with(id: :age).(17).to_ast).to eql( [:failure, [:age, [:or, [ [:predicate, [:nil?, [[:input, 17]]]], [:predicate, [:gt?, [[:num, 18], [:input, 17]]]] ]]]] ) end end describe "#and" do it "creates and with the other" do expect(operation.and(other).(nil)).to be_failure expect(operation.and(other).(19)).to be_failure expect(operation.and(other).(13)).to be_failure expect(operation.and(other).(14)).to be_failure end end describe "#or" do it "creates or with the other" do expect(operation.or(other).(nil)).to be_success expect(operation.or(other).(19)).to be_success expect(operation.or(other).(13)).to be_success expect(operation.or(other).(14)).to be_failure end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("nil? OR gt?(18)") end end end ================================================ FILE: spec/unit/operations/set_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Set do subject(:operation) { described_class.new(is_int, gt_18) } include_context "predicates" let(:is_int) { Dry::Logic::Rule::Predicate.build(int?) } let(:gt_18) { Dry::Logic::Rule::Predicate.build(gt?, args: [18]) } describe "#call" do it "applies all its rules to the input" do expect(operation.(19)).to be_success expect(operation.(17)).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:set, [[:predicate, [:int?, [[:input, undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, undefined]]]]]] ) end it "returns result ast" do expect(operation.(17).to_ast).to eql( [:set, [[:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]] ) end it "returns result ast with an :id" do expect(operation.with(id: :age).(17).to_ast).to eql( [:failure, [:age, [:set, [[:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]]]] ) end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("set(int?, gt?(18))") end end end ================================================ FILE: spec/unit/operations/xor_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Operations::Xor do subject(:operation) { described_class.new(left, right) } include_context "predicates" let(:left) { Dry::Logic::Rule::Predicate.build(array?) } let(:right) { Dry::Logic::Rule::Predicate.build(empty?) } let(:other) do Dry::Logic::Rule::Predicate.build(str?) end describe "#call" do it "calls left and right" do expect(operation.(nil)).to be_success expect(operation.("")).to be_success expect(operation.([])).to be_failure end end describe "#to_ast" do it "returns ast" do expect(operation.to_ast).to eql( [:xor, [[:predicate, [:array?, [[:input, undefined]]]], [:predicate, [:empty?, [[:input, undefined]]]]]] ) end it "returns result ast" do expect(operation.([]).to_ast).to eql( [:xor, [[:predicate, [:array?, [[:input, []]]]], [:predicate, [:empty?, [[:input, []]]]]]] ) end it "returns failure result ast" do expect(operation.with(id: :name).([]).to_ast).to eql( [:failure, [:name, [:xor, [[:predicate, [:array?, [[:input, []]]]], [:predicate, [:empty?, [[:input, []]]]]]]]] ) end end describe "#and" do it "creates conjunction with the other" do expect(operation.and(other).(nil)).to be_failure expect(operation.and(other).(19)).to be_failure expect(operation.and(other).("")).to be_success end end describe "#or" do it "creates disjunction with the other" do expect(operation.or(other).([])).to be_failure expect(operation.or(other).("")).to be_success end end describe "#to_s" do it "returns string representation" do expect(operation.to_s).to eql("array? XOR empty?") end end end ================================================ FILE: spec/unit/predicates/array_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#array?" do let(:predicate_name) { :array? } context "when value is an array" do let(:arguments_list) do [ [[]], [%w[other array]], [[123, "really", :blah]], [[]], [[nil]], [[false]], [[true]] ] end it_behaves_like "a passing predicate" end context "when value is not an array" do let(:arguments_list) do [ [""], [{}], [nil], [:symbol], [String], [1], [1.0], [true], [{}] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/attr_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#attr?" do let(:predicate_name) { :attr? } context "when value responds to the attr name" do let(:arguments_list) do [ [:name, Struct.new(:name).new("John")], [:age, Struct.new(:age).new(18)] ] end it_behaves_like "a passing predicate" end context "with value does not respond to the attr name" do let(:arguments_list) do [ [:name, Struct.new(:age).new(18)], [:age, Struct.new(:name).new("Jill")] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/bool_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#bool?" do let(:predicate_name) { :bool? } context "when value is a boolean" do let(:arguments_list) do [[true], [false]] end it_behaves_like "a passing predicate" end context "when value is not a bool" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1], [0], ["true"], ["false"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/bytesize_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#bytesize?" do let(:predicate_name) { :bytesize? } context "when value size is equal to n" do let(:arguments_list) do [ [4, "こa"], [1..8, "こa"] ] end it_behaves_like "a passing predicate" end context "when value size is greater than n" do let(:arguments_list) do [ [3, "こa"], [1..3, "こa"] ] end it_behaves_like "a failing predicate" end context "with value size is less than n" do let(:arguments_list) do [ [5, "こa"], [5..10, "こa"] ] end it_behaves_like "a failing predicate" end context "with an unsupported size" do it "raises an error" do expect { Dry::Logic::Predicates[:bytesize?].call("oops", 1) }.to raise_error(ArgumentError, /oops/) end end end end ================================================ FILE: spec/unit/predicates/case_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#case?" do let(:predicate_name) { :case? } context "when the value matches the pattern" do let(:arguments_list) do [ [11, 11], [:odd?.to_proc, 11], [/\Af/, "foo"], [Integer, 11] ] end it_behaves_like "a passing predicate" end context "when the value doesn't match the pattern" do let(:arguments_list) do [ [13, 14], [:odd?.to_proc, 12], [/\Af/, "bar"], [String, 11] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/date_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#date?" do let(:predicate_name) { :date? } context "when value is a date" do let(:arguments_list) do [[Date.today]] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/date_time_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#date_time?" do let(:predicate_name) { :date_time? } context "when value is a datetime" do let(:arguments_list) do [[DateTime.now]] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/decimal_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#decimal?" do let(:predicate_name) { :decimal? } context "when value is a decimal" do let(:arguments_list) do [[1.2.to_d]] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1], [1.0] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/empty_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#empty?" do let(:predicate_name) { :empty? } context "when value is empty" do let(:arguments_list) do [ [""], [[]], [{}], [nil] ] end it_behaves_like "a passing predicate" end context "with value is not empty" do let(:arguments_list) do [ ["Jill"], [[1, 2, 3]], [{name: "John"}], [true], [false], ["1"], ["0"], [:symbol], [String] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/eql_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates, "#eql?" do let(:predicate_name) { :eql? } context "when value is equal to the arg" do let(:arguments_list) do [%w[Foo Foo]] end it_behaves_like "a passing predicate" end context "with value is not equal to the arg" do let(:arguments_list) do [%w[Bar Foo]] end it_behaves_like "a failing predicate" end end ================================================ FILE: spec/unit/predicates/even_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#even?" do let(:predicate_name) { :even? } context "when value is an odd int" do let(:arguments_list) do [ [13], [1], [1111] ] end it_behaves_like "a failing predicate" end context "with value is an even int" do let(:arguments_list) do [ [0], [2], [2222] ] end it_behaves_like "a passing predicate" end end end ================================================ FILE: spec/unit/predicates/excluded_from_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#excluded_from?" do let(:predicate_name) { :excluded_from? } context "when value is not present in list" do let(:arguments_list) do [ [%w[Jill John], "Jack"], [1..2, 0], [1..2, 3], [[nil, false], true] ] end it_behaves_like "a passing predicate" end context "with value is present in list" do let(:arguments_list) do [ [%w[Jill John], "Jill"], [%w[Jill John], "John"], [1..2, 1], [1..2, 2], [[nil, false], nil], [[nil, false], false] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/excludes_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#excludes?" do let(:predicate_name) { :excludes? } context "with input excludes value" do let(:arguments_list) do [ ["Jack", %w[Jill John]], [0, 1..2], [3, 1..2], ["foo", "Hello World"], [:foo, {bar: 0}], [true, [nil, false]] ] end it_behaves_like "a passing predicate" end context "with input of invalid type" do let(:arguments_list) do [ [2, 1], [1, nil], ["foo", 1], [1, "foo"], [1..2, "foo"], ["foo", 1..2], [:key, "foo"] ] end it_behaves_like "a passing predicate" end context "when input includes value" do let(:arguments_list) do [ ["Jill", %w[Jill John]], ["John", %w[Jill John]], [1, 1..2], [2, 1..2], ["Hello", "Hello World"], ["World", "Hello World"], [:bar, {bar: 0}], [nil, [nil, false]], [false, [nil, false]] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/false_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#false?" do let(:predicate_name) { :false? } context "when value is false" do let(:arguments_list) do [[false]] end it_behaves_like "a passing predicate" end context "when value is not false" do let(:arguments_list) do [ [true], [""], [[]], [{}], [nil], [:symbol], [String], [1], [0], ["true"], ["false"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/filled_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#filled?" do let(:predicate_name) { :filled? } context "when value is filled" do let(:arguments_list) do [ ["Jill"], [[1, 2, 3]], [{name: "John"}], [true], [false], ["1"], ["0"], [:symbol], [String] ] end it_behaves_like "a passing predicate" end context "with value is not filled" do let(:arguments_list) do [ [""], [[]], [{}], [nil] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/float_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#float?" do let(:predicate_name) { :float? } context "when value is a float" do let(:arguments_list) do [[1.0]] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/format_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates, "#format?" do let(:predicate_name) { :format? } context "when value matches provided regexp" do let(:arguments_list) do [[/^F/, "Foo"]] end it_behaves_like "a passing predicate" end context "when value does not match provided regexp" do let(:arguments_list) do [[/^F/, "Bar"]] end it_behaves_like "a failing predicate" end context "when input is nil" do let(:arguments_list) do [[/^F/, nil]] end it_behaves_like "a failing predicate" end end ================================================ FILE: spec/unit/predicates/gt_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#gt?" do let(:predicate_name) { :gt? } context "when value is greater than n" do let(:arguments_list) do [ [13, 14], [13.37, 13.38] ] end it_behaves_like "a passing predicate" end context "when value is equal to n" do let(:arguments_list) do [ [13, 13], [13.37, 13.37] ] end it_behaves_like "a failing predicate" end context "with value is less than n" do let(:arguments_list) do [ [13, 12], [13.37, 13.36] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/gteq_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#gteq?" do let(:predicate_name) { :gteq? } context "when value is greater than n" do let(:arguments_list) do [ [13, 14], [13.37, 13.38] ] end it_behaves_like "a passing predicate" end context "when value is equal to n" do let(:arguments_list) do [ [13, 13], [13.37, 13.37] ] end it_behaves_like "a passing predicate" end context "with value is less than n" do let(:arguments_list) do [ [13, 12], [13.37, 13.36] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/hash_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#hash?" do let(:predicate_name) { :hash? } context "when value is a hash" do let(:arguments_list) do [ [{}], [foo: :bar], [{}] ] end it_behaves_like "a passing predicate" end context "when value is not a hash" do let(:arguments_list) do [ [""], [[]], [nil], [:symbol], [String], [1], [1.0], [true], [[]], [Hash] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/included_in_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#included_in?" do let(:predicate_name) { :included_in? } context "when value is present in list" do let(:arguments_list) do [ [%w[Jill John], "Jill"], [%w[Jill John], "John"], [1..2, 1], [1..2, 2], [[nil, false], nil], [[nil, false], false] ] end it_behaves_like "a passing predicate" end context "with value is not present in list" do let(:arguments_list) do [ [%w[Jill John], "Jack"], [1..2, 0], [1..2, 3], [[nil, false], true] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/includes_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates, "#is?" do let(:predicate_name) { :is? } let(:one) { Object.new } let(:two) { Object.new } context "when value is equal to the arg" do let(:arguments_list) do [[one, one], [:one, :one]] end it_behaves_like "a passing predicate" end context "with value is not equal to the arg" do let(:arguments_list) do [[one, two], [{}, {}]] end it_behaves_like "a failing predicate" end end ================================================ FILE: spec/unit/predicates/int_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#int?" do let(:predicate_name) { :int? } context "when value is an integer" do let(:arguments_list) do [ [1], [33], [7] ] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/key_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#key?" do let(:predicate_name) { :key? } context "when key is present in value" do let(:arguments_list) do [ [:name, {name: "John"}], [:age, {age: 18}] ] end it_behaves_like "a passing predicate" end context "with key is not present in value" do let(:arguments_list) do [ [:name, {age: 18}], [:age, {name: "Jill"}] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/lt_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#lt?" do let(:predicate_name) { :lt? } context "when value is less than n" do let(:arguments_list) do [ [13, 12], [13.37, 13.36] ] end it_behaves_like "a passing predicate" end context "when value is equal to n" do let(:arguments_list) do [ [13, 13], [13.37, 13.37] ] end it_behaves_like "a failing predicate" end context "with value is greater than n" do let(:arguments_list) do [ [13, 14], [13.37, 13.38] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/lteq_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#lteq?" do let(:predicate_name) { :lteq? } context "when value is less than n" do let(:arguments_list) do [ [13, 12], [13.37, 13.36] ] end it_behaves_like "a passing predicate" end context "when value is equal to n" do let(:arguments_list) do [ [13, 13], [13.37, 13.37] ] end it_behaves_like "a passing predicate" end context "with value is greater than n" do let(:arguments_list) do [ [13, 14], [13.37, 13.38] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/max_bytesize_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#max_bytesize?" do let(:predicate_name) { :max_bytesize? } context "when value size is less than n" do let(:arguments_list) do [ [5, "こa"] ] end it_behaves_like "a passing predicate" end context "when value size is equal to n" do let(:arguments_list) do [ [5, "こab"] ] end it_behaves_like "a passing predicate" end context "with value size is greater than n" do let(:arguments_list) do [ [5, "こabc"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/max_size_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#max_size?" do let(:predicate_name) { :max_size? } context "when value size is less than n" do let(:arguments_list) do [ [3, [1, 2]], [5, "Jill"], [3, {1 => "st", 2 => "nd"}], [6, 1..5] ] end it_behaves_like "a passing predicate" end context "when value size is equal to n" do let(:arguments_list) do [ [2, [1, 2]], [4, "Jill"], [2, {1 => "st", 2 => "nd"}], [5, 1..5] ] end it_behaves_like "a passing predicate" end context "with value size is greater than n" do let(:arguments_list) do [ [1, [1, 2]], [3, "Jill"], [1, {1 => "st", 2 => "nd"}], [4, 1..5] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/min_bytesize_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#min_bytesize?" do let(:predicate_name) { :min_bytesize? } context "when value size is greater than n" do let(:arguments_list) do [ [3, "こa"] ] end it_behaves_like "a passing predicate" end context "when value size is equal to n" do let(:arguments_list) do [ [5, "こab"] ] end it_behaves_like "a passing predicate" end context "with value size is less than n" do let(:arguments_list) do [ [5, "こ"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/min_size_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#min_size?" do let(:predicate_name) { :min_size? } context "when value size is greater than n" do let(:arguments_list) do [ [1, [1, 2]], [3, "Jill"], [1, {1 => "st", 2 => "nd"}], [4, 1..5] ] end it_behaves_like "a passing predicate" end context "when value size is equal to n" do let(:arguments_list) do [ [2, [1, 2]], [4, "Jill"], [2, {1 => "st", 2 => "nd"}], [5, 1..5] ] end it_behaves_like "a passing predicate" end context "with value size is less than n" do let(:arguments_list) do [ [3, [1, 2]], [5, "Jill"], [3, {1 => "st", 2 => "nd"}], [6, 1..5] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/none_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#nil?" do let(:predicate_name) { :nil? } context "when value is nil" do let(:arguments_list) { [[nil]] } it_behaves_like "a passing predicate" end context "when value is not nil" do let(:arguments_list) do [ [""], [true], [false], [0], [:symbol], [[]], [{}], [String] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/not_eql_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates, "#not_eql?" do let(:predicate_name) { :not_eql? } context "when value is equal to the arg" do let(:arguments_list) do [%w[Foo Foo]] end it_behaves_like "a failing predicate" end context "with value is not equal to the arg" do let(:arguments_list) do [%w[Bar Foo]] end it_behaves_like "a passing predicate" end end ================================================ FILE: spec/unit/predicates/number_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#number?" do let(:predicate_name) { :number? } context "when value is numerical" do let(:arguments_list) do [ ["34"], ["1.000004"], ["0"], [4], ["-15.24"], [-3.5] ] end it_behaves_like "a passing predicate" end context "with value is not numerical" do let(:arguments_list) do [ [""], ["-14px"], ["10,150.00"], [nil], [:symbol], [String] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/odd_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#odd?" do let(:predicate_name) { :odd? } context "when value is an odd int" do let(:arguments_list) do [ [13], [1], [1111] ] end it_behaves_like "a passing predicate" end context "with value is an even int" do let(:arguments_list) do [ [0], [2], [2222] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/respond_to_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#respond_to?" do let(:predicate_name) { :respond_to? } context "when value responds to method" do let(:arguments_list) do [ [:method, Object], [:new, Hash] ] end it_behaves_like "a passing predicate" end context "when value does not respond to method" do let(:arguments_list) do [ [:foo, Object], [:bar, Hash] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/size_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#size?" do let(:predicate_name) { :size? } context "when value size is equal to n" do let(:arguments_list) do [ [[2, 4, 6], "abcd"], [4, "Jill"], [2, {1 => "st", 2 => "nd"}], [1..8, "qwerty"] ] end it_behaves_like "a passing predicate" end context "when value size is greater than n" do let(:arguments_list) do [ [[1, 2], "abc"], [5, "Jill"], [3, {1 => "st", 2 => "nd"}], [1..5, "qwerty"] ] end it_behaves_like "a failing predicate" end context "with value size is less than n" do let(:arguments_list) do [ [[1, 2], 1], [3, "Jill"], [1, {1 => "st", 2 => "nd"}], [1..5, "qwerty"] ] end it_behaves_like "a failing predicate" end context "with an unsupported size" do it "raises an error" do expect { Dry::Logic::Predicates[:size?].call("oops", 1) }.to raise_error(ArgumentError, /oops/) end end end end ================================================ FILE: spec/unit/predicates/str_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#str?" do let(:predicate_name) { :str? } context "when value is a string" do let(:arguments_list) do [ [""], ["John"] ] end it_behaves_like "a passing predicate" end context "with value is not a string" do let(:arguments_list) do [ [[]], [{}], [nil], [:symbol], [String] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/time_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#time?" do let(:predicate_name) { :time? } context "when value is a time" do let(:arguments_list) do [[Time.now]] end it_behaves_like "a passing predicate" end context "with value is not an integer" do let(:arguments_list) do [ [""], [[]], [{}], [nil], [:symbol], [String], [1] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/true_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#true?" do let(:predicate_name) { :true? } context "when value is true" do let(:arguments_list) do [[true]] end it_behaves_like "a passing predicate" end context "with value is not true" do let(:arguments_list) do [ [false], [""], [[]], [{}], [nil], [:symbol], [String], [1], [0], ["true"], ["false"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/type_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#type?" do let(:predicate_name) { :type? } context "when value has a correct type" do let(:arguments_list) do [[TrueClass, true]] end it_behaves_like "a passing predicate" end context "with value is not true" do let(:arguments_list) do [ [TrueClass, false], [TrueClass, ""], [TrueClass, []], [TrueClass, {}], [TrueClass, nil], [TrueClass, :symbol], [TrueClass, String], [TrueClass, 1], [TrueClass, 0], [TrueClass, "true"], [TrueClass, "false"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uri_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uri?" do let(:predicate_name) { :uri? } context "when value is a valid URI" do let(:arguments_list) do [ [nil, "https://github.com/dry-rb/dry-logic"], # without schemes param ["https", "https://github.com/dry-rb/dry-logic"], # with scheme param [%w[http https], "https://github.com/dry-rb/dry-logic"], # with schemes array ["mailto", "mailto:myemail@host.com"], # with mailto format ["urn", "urn:isbn:0451450523"] # with URN format ] end it_behaves_like "a passing predicate" end context "with value is not a valid URI" do let(:arguments_list) do [ ["http", "mailto:myemail@host.com"], # scheme not allowed [%w[http https], "ftp:://myftp.com"], # scheme not allowed ["", "not-a-uri-at-all"] ] end it_behaves_like "a failing predicate" end end describe "#uri_rfc3986?" do let(:predicate_name) { :uri_rfc3986? } context "when value is a valid URI" do let(:arguments_list) do [ ["https://github.com/dry-rb/dry-logic"], # with https format ["mailto:myemail@host.com"], # with mailto format ["urn:isbn:0451450523"] # with URN format ] end it_behaves_like "a passing predicate" end context "with value is not a valid URI" do let(:arguments_list) do [ ["not-a-uri-at-all"], ["[https://github.com/dry-rb/dry-logic]"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v1_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v1?" do let(:predicate_name) { :uuid_v1? } context "when value is a valid V1 UUID" do let(:arguments_list) do [["f2d26c57-e07c-1416-a749-57e937930e04"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V1 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\nf2d26c57-e07c-1416-a749-57e937930e04"], # V1 with invalid prefix ["f2d26c57-e07c-1416-a749-57e937930e04\nnot-a-uuid-at-all"], # V1 with invalid suffix ["f2d26c57-e07c-3416-a749-57e937930e04"], # wrong version number (3, not 1) ["20633928-6a07-41e9-a923-1681be663d3e"], # UUID V4 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v2_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v2?" do let(:predicate_name) { :uuid_v2? } context "when value is a valid V1 UUID" do let(:arguments_list) do [["f2d26c57-e07c-2416-a749-57e937930e04"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V4 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\nf2d26c57-e07c-2416-a749-57e937930e04"], # V2 with invalid prefix ["f2d26c57-e07c-2416-a749-57e937930e04\nnot-a-uuid-at-all"], # V2 with invalid suffix ["f2d26c57-e07c-3416-a749-57e937930e04"], # wrong version number (3, not 2) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v3_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v3?" do let(:predicate_name) { :uuid_v3? } context "when value is a valid V3 UUID" do let(:arguments_list) do [["f2d26c57-e07c-3416-a749-57e937930e04"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V4 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\nf2d26c57-e07c-3416-a749-57e937930e04"], # V3 with invalid prefix ["f2d26c57-e07c-3416-a749-57e937930e04\nnot-a-uuid-at-all"], # V3 with invalid suffix ["f2d26c57-e07c-4416-a749-57e937930e04"], # wrong version number (4, not 3) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v4_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v4?" do let(:predicate_name) { :uuid_v4? } context "when value is a valid V4 UUID" do let(:arguments_list) do [["f2d26c57-e07c-4416-a749-57e937930e04"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V4 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\nf2d26c57-e07c-4416-a749-57e937930e04"], # V4 with invalid prefix ["f2d26c57-e07c-4416-a749-57e937930e04\nnot-a-uuid-at-all"], # V4 with invalid suffix ["f2d26c57-e07c-3416-a749-57e937930e04"], # wrong version number (3, not 4) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v5_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v5?" do let(:predicate_name) { :uuid_v5? } context "when value is a valid V5 UUID" do let(:arguments_list) do [["f2d26c57-e07c-5416-a749-57e937930e04"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V5 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\nf2d26c57-e07c-5416-a749-57e937930e04"], # V5 with invalid prefix ["f2d26c57-e07c-5416-a749-57e937930e04\nnot-a-uuid-at-all"], # V5 with invalid suffix ["f2d26c57-e07c-3416-a749-57e937930e04"], # wrong version number (3, not 5) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v6_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v6?" do let(:predicate_name) { :uuid_v6? } context "when value is a valid V6 UUID" do let(:arguments_list) do [["1ec9414c-232a-6b00-b3c8-9e6bdeced846"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V6 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\n1ec9414c-232a-6b00-b3c8-9e6bdeced846"], # V6 with invalid prefix ["1ec9414c-232a-6b00-b3c8-9e6bdeced846\nnot-a-uuid-at-all"], # V6 with invalid suffix ["1ec9414c-232a-3b00-b3c8-9e6bdeced846"], # wrong version number (3, not 6) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v7_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v7?" do let(:predicate_name) { :uuid_v7? } context "when value is a valid V7 UUID" do let(:arguments_list) do [["017f22e2-79b0-7cc3-98c4-dc0c0c07398f"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V7 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\n017f22e2-79b0-7cc3-98c4-dc0c0c07398f"], # V6 with invalid prefix ["017f22e2-79b0-7cc3-98c4-dc0c0c07398f\nnot-a-uuid-at-all"], # V6 with invalid suffix ["017f22e2-79b0-4cc3-98c4-dc0c0c07398f"], # wrong version number (4, not 7) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates/uuid_v8_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do describe "#uuid_v8?" do let(:predicate_name) { :uuid_v8? } context "when value is a valid V8 UUID" do let(:arguments_list) do [["320c3d4d-cc00-875b-8ec9-32d5f69181c0"]] end it_behaves_like "a passing predicate" end context "with value is not a valid V8 UUID" do let(:arguments_list) do [ ["not-a-uuid-at-all\n320c3d4d-cc00-875b-8ec9-32d5f69181c0"], # V6 with invalid prefix ["320c3d4d-cc00-875b-8ec9-32d5f69181c0\nnot-a-uuid-at-all"], # V6 with invalid suffix ["320c3d4d-cc00-475b-8ec9-32d5f69181c0"], # wrong version number (4, not 8) ["20633928-6a07-11e9-a923-1681be663d3e"], # UUID V1 ["not-a-uuid-at-all"] ] end it_behaves_like "a failing predicate" end end end ================================================ FILE: spec/unit/predicates_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/predicates" RSpec.describe Dry::Logic::Predicates do it "can be included in another module" do mod = Module.new { include Dry::Logic::Predicates } expect(mod[:key?]).to be_a(Method) end describe ".predicate" do it "defines a predicate method" do mod = Module.new { include Dry::Logic::Predicates predicate(:test?) do |foo| true end } expect(mod.test?("arg")).to be(true) end end describe ".respond_to?" do it "works with a just the method name" do expect(Dry::Logic::Predicates.respond_to?(:predicate)).to be(true) expect(Dry::Logic::Predicates.respond_to?(:not_here)).to be(false) end end describe ".eql?" do it "works with another object to compare to" do expect(Dry::Logic::Predicates.eql?(Dry::Logic::Predicates)).to be(true) expect(Dry::Logic::Predicates.eql?("something else")).to be(false) end end end ================================================ FILE: spec/unit/rule/predicate_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Rule::Predicate do subject(:rule) { described_class.build(predicate) } let(:predicate) { str? } include_context "predicates" it_behaves_like Dry::Logic::Rule describe "#name" do it "returns predicate identifier" do expect(rule.name).to be(:str?) end end describe "#to_ast" do context "without a result" do it "returns rule ast" do expect(rule.to_ast).to eql([:predicate, [:str?, [[:input, undefined]]]]) end it "returns :failure with an id" do email = rule.with(id: :email) expect(email.(11).to_ast).to eql([:failure, [:email, [:predicate, [:str?, [[:input, 11]]]]]]) end end context "with a result" do it "returns success" do expect(rule.("foo")).to be_success end it "returns failure ast" do expect(rule.(5).to_ast).to eql([:predicate, [:str?, [[:input, 5]]]]) end end context "with a zero-arity predicate" do let(:predicate) { Module.new { def self.test? true end } .method(:test?) } it "returns ast" do expect(rule.to_ast).to eql([:predicate, [:test?, []]]) end end end describe "#to_s" do it "returns string representation" do expect(rule.curry("foo").to_s).to eql('str?("foo")') end end end ================================================ FILE: spec/unit/rule_compiler_spec.rb ================================================ # frozen_string_literal: true require "dry/logic/rule_compiler" RSpec.describe Dry::Logic::RuleCompiler, "#call" do subject(:compiler) { described_class.new(predicates) } let(:predicates) { {key?: predicate, attr?: predicate, filled?: predicate, gt?: predicate, one: predicate} } let(:predicate) { double(:predicate, name: :test?, arity: 2).as_null_object } let(:rule) { Dry::Logic::Rule::Predicate.build(predicate) } let(:key_op) { Dry::Logic::Operations::Key.new(rule, name: :email) } let(:attr_op) { Dry::Logic::Operations::Attr.new(rule, name: :email) } let(:check_op) { Dry::Logic::Operations::Check.new(rule, keys: [:email]) } let(:not_key_op) { Dry::Logic::Operations::Negation.new(key_op) } let(:and_op) { key_op.curry(:email) & rule } let(:or_op) { key_op.curry(:email) | rule } let(:xor_op) { key_op.curry(:email) ^ rule } let(:set_op) { Dry::Logic::Operations::Set.new(rule) } let(:each_op) { Dry::Logic::Operations::Each.new(rule) } it "compiles key rules" do ast = [[:key, [:email, [:predicate, [:filled?, [[:input, undefined]]]]]]] rules = compiler.(ast) expect(rules).to eql([key_op]) end it "compiles attr rules" do ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, undefined]]]]]]] rules = compiler.(ast) expect(rules).to eql([attr_op]) end it "compiles check rules" do ast = [[:check, [[:email], [:predicate, [:filled?, [[:input, undefined]]]]]]] rules = compiler.(ast) expect(rules).to eql([check_op]) end it "compiles attr rules" do ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, undefined]]]]]]] rules = compiler.(ast) expect(rules).to eql([attr_op]) end it "compiles negated rules" do ast = [[:not, [:key, [:email, [:predicate, [:filled?, [[:input, undefined]]]]]]]] rules = compiler.(ast) expect(rules).to eql([not_key_op]) end it "compiles and rules" do ast = [ [ :and, [ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, undefined]]]]]], [:predicate, [:filled?, [[:input, undefined]]]] ] ] ] rules = compiler.(ast) expect(rules).to eql([and_op]) end it "compiles or rules" do ast = [ [ :or, [ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, undefined]]]]]], [:predicate, [:filled?, [[:input, undefined]]]] ] ] ] rules = compiler.(ast) expect(rules).to eql([or_op]) end it "compiles exclusive or rules" do ast = [ [ :xor, [ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, undefined]]]]]], [:predicate, [:filled?, [[:input, undefined]]]] ] ] ] rules = compiler.(ast) expect(rules).to eql([xor_op]) end it "compiles set rules" do ast = [[:set, [[:predicate, [:filled?, [[:input, nil]]]]]]] rules = compiler.(ast) expect(rules).to eql([set_op]) end it "compiles each rules" do ast = [[:each, [:predicate, [:filled?, [[:input, nil]]]]]] rules = compiler.(ast) expect(rules).to eql([each_op]) end end ================================================ FILE: spec/unit/rule_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::Logic::Rule do subject(:rule) { described_class.build(predicate, **options) } let(:predicate) { -> { true } } let(:options) { {} } let(:schema) do Class.new do define_method(:class, Kernel.instance_method(:class)) def respond_to_missing?(m, *) super || m.to_s.end_with?("?") end def method_missing(m, *) if m.to_s.end_with?("?") self.class.new else super end end def to_proc -> value { value } end def arity 1 end def parameters [[:req, :value]] end end.new end it_behaves_like described_class describe ".new" do it "accepts an :id" do expect(described_class.build(predicate, id: :check_num).id).to be(:check_num) end end describe "with a function returning truthy value" do it "is successful for valid input" do expect(described_class.build(-> val { val }).("true")).to be_success end it "is not successful for invalid input" do expect(described_class.build(-> val { val }).(nil)).to be_failure end end describe "#ast" do it "returns predicate node with :id" do expect(described_class.build(-> value { true }).with(id: :email?).ast("oops")).to eql( [:predicate, [:email?, [[:value, "oops"]]]] ) end it "returns predicate node with undefined args" do expect(described_class.build(-> value { true }).with(id: :email?).ast).to eql( [:predicate, [:email?, [[:value, undefined]]]] ) end end describe "#type" do it "returns rule type" do expect(rule.type).to be(:rule) end end describe "#bind" do let(:bound) { rule.with(id: :bound).bind(object) } context "with an unbound method" do let(:predicate) { klass.instance_method(:test?) } let(:klass) { Class.new { def test? true end } } let(:object) { klass.new } it "returns a new rule with its predicate bound to a specific object" do expect(bound.()).to be_success end it "carries id" do expect(bound.id).to be(:bound) end end context "with an arbitrary block" do let(:predicate) { -> value { value == expected } } let(:object) { Class.new { def expected "test" end } .new } it "returns a new with its predicate executed in the context of the provided object" do expect(bound.("test")).to be_success expect(bound.("oops")).to be_failure end it "carries id" do expect(bound.id).to be(:bound) end it "stores arity" do expect(bound.options[:arity]).to be(rule.arity) end it "stores parameters" do expect(bound.options[:parameters]).to eql(rule.parameters) end end context "with a schema instance" do let(:object) { schema } let(:predicate) { schema } it "returns a new with its predicate executed in the context of the provided object" do expect(bound.(true)).to be_success expect(bound.(false)).to be_failure end end end describe "#eval_args" do context "with an unbound method" do let(:options) { {args: [1, klass.instance_method(:num), :foo], arity: 3} } let(:klass) { Class.new { def num 7 end } } let(:object) { klass.new } it "evaluates args in the context of the provided object" do expect(rule.eval_args(object).args).to eql([1, 7, :foo]) end end context "with a schema instance" do let(:options) { {args: [1, schema, :foo], arity: 3} } let(:object) { Object.new } it "returns a new with its predicate executed in the context of the provided object" do expect(rule.eval_args(object).args).to eql([1, schema, :foo]) end end end describe "arity specialization" do describe "0-arity rule" do let(:options) { {args: [1], arity: 1} } let(:predicate) { :odd?.to_proc } it "generates interface with the right arity" do expect(rule.method(:call).arity).to be_zero expect(rule.method(:[]).arity).to be_zero expect(rule[]).to be(true) expect(rule.()).to be_success end end describe "1-arity rule" do let(:options) { {args: [1], arity: 2} } let(:predicate) { -> a, b { a + b } } it "generates interface with the right arity" do expect(rule.method(:call).arity).to be(1) expect(rule.method(:[]).arity).to be(1) expect(rule[10]).to be(11) expect(rule.(1)).to be_success end end describe "currying" do let(:options) { {args: [], arity: 2} } let(:predicate) { -> a, b { a + b } } let(:rule) { super().curry(1) } it "generates correct arity on currying" do expect(rule.method(:call).arity).to be(1) expect(rule.method(:[]).arity).to be(1) expect(rule[10]).to be(11) expect(rule.(1)).to be_success end end describe "arbitrary arity" do let(:arity) { rand(1..20) } let(:curried) { rand(arity) } let(:options) { {args: [1] * curried, arity: arity} } let(:predicate) { double(:predicate) } it "generates correct arity" do expect(rule.method(:call).arity).to be(arity - curried) expect(rule.method(:[]).arity).to be(arity - curried) end end describe "-1 arity" do let(:options) { {args: [], arity: -1} } it "accepts variable number of arguments" do expect(rule.method(:call).arity).to be(-1) expect(rule.method(:[]).arity).to be(-1) end end describe "-2 arity" do let(:options) { {args: [], arity: -2} } it "accepts variable number of arguments" do expect(rule.method(:call).arity).to be(-2) expect(rule.method(:[]).arity).to be(-2) end context "curried 1" do let(:options) { {args: [1], arity: -2} } it "doesn't have required arguments" do expect(rule.method(:call).arity).to be(-1) expect(rule.method(:[]).arity).to be(-1) end end context "curried 2" do let(:options) { {args: [1, 2], arity: -2} } it "doesn't have required arguments" do expect(rule.method(:call).arity).to be(-1) expect(rule.method(:[]).arity).to be(-1) end end end describe "constants" do let(:options) { {args: [], arity: 0} } it "accepts variable number of arguments" do expect(rule.method(:call).arity).to be(-1) expect(rule.method(:[]).arity).to be(-1) end end end end ================================================ FILE: zizmor.yml ================================================ rules: unpinned-uses: config: policies: hanakai-rb/*: ref-pin