├── .codeclimate.yml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── custom │ └── ci.yml │ ├── docsite.yml │ └── sync_configs.yml ├── .gitignore ├── .inch.yml ├── .rspec ├── .rubocop.yml ├── .simplecov ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── bundle ├── console └── doc ├── changeset ├── .rspec ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── docsite │ └── source │ │ ├── associations.html.md │ │ ├── index.html.md │ │ └── mapping.html.md ├── lib │ ├── rom-changeset.rb │ └── rom │ │ ├── changeset.rb │ │ ├── changeset │ │ ├── associated.rb │ │ ├── create.rb │ │ ├── delete.rb │ │ ├── extensions │ │ │ └── relation.rb │ │ ├── pipe.rb │ │ ├── pipe_registry.rb │ │ ├── stateful.rb │ │ ├── update.rb │ │ └── version.rb │ │ └── plugins │ │ └── relation │ │ └── changeset.rb ├── log │ └── .gitkeep ├── rom-changeset.gemspec └── spec │ ├── integration │ ├── changeset_spec.rb │ ├── command_options_spec.rb │ └── plugin_spec.rb │ ├── shared │ ├── database.rb │ ├── mappers.rb │ ├── models.rb │ ├── plugins.rb │ ├── relations.rb │ ├── repo.rb │ ├── seeds.rb │ └── structs.rb │ ├── spec_helper.rb │ └── unit │ ├── associate_spec.rb │ ├── changeset_spec.rb │ ├── create_spec.rb │ ├── delete_spec.rb │ ├── map_spec.rb │ ├── pipe_registry_spec.rb │ ├── pipe_spec.rb │ └── update_spec.rb ├── core ├── .rspec ├── .simplecov ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks │ ├── basic_bench.rb │ ├── gc_suite.rb │ ├── profile_combine.rb │ ├── profile_one.rb │ ├── profile_wrap.rb │ └── setup.rb ├── docsite │ └── source │ │ ├── associations.html.md │ │ ├── combines.html.md │ │ ├── commands.html.md │ │ ├── framework-setup.html.md │ │ ├── index.html.md │ │ ├── mappers.html.md │ │ ├── quick-setup.html.md │ │ ├── relations.html.md │ │ ├── schemas.html.md │ │ └── structs.html.md ├── lib │ ├── rom-core.rb │ └── rom │ │ ├── array_dataset.rb │ │ ├── association_set.rb │ │ ├── associations │ │ ├── abstract.rb │ │ ├── definitions.rb │ │ ├── definitions │ │ │ ├── abstract.rb │ │ │ ├── many_to_many.rb │ │ │ ├── many_to_one.rb │ │ │ ├── one_to_many.rb │ │ │ ├── one_to_one.rb │ │ │ └── one_to_one_through.rb │ │ ├── many_to_many.rb │ │ ├── many_to_one.rb │ │ ├── one_to_many.rb │ │ ├── one_to_one.rb │ │ ├── one_to_one_through.rb │ │ └── through_identifier.rb │ │ ├── attribute.rb │ │ ├── auto_curry.rb │ │ ├── cache.rb │ │ ├── command.rb │ │ ├── command_compiler.rb │ │ ├── command_proxy.rb │ │ ├── command_registry.rb │ │ ├── commands.rb │ │ ├── commands │ │ ├── class_interface.rb │ │ ├── composite.rb │ │ ├── create.rb │ │ ├── delete.rb │ │ ├── graph.rb │ │ ├── graph │ │ │ ├── class_interface.rb │ │ │ └── input_evaluator.rb │ │ ├── lazy.rb │ │ ├── lazy │ │ │ ├── create.rb │ │ │ ├── delete.rb │ │ │ └── update.rb │ │ └── update.rb │ │ ├── configuration.rb │ │ ├── configuration_dsl.rb │ │ ├── configuration_dsl │ │ ├── command.rb │ │ ├── command_dsl.rb │ │ ├── mapper_dsl.rb │ │ └── relation.rb │ │ ├── constants.rb │ │ ├── container.rb │ │ ├── core.rb │ │ ├── core │ │ └── version.rb │ │ ├── create_container.rb │ │ ├── data_proxy.rb │ │ ├── enumerable_dataset.rb │ │ ├── environment.rb │ │ ├── gateway.rb │ │ ├── global.rb │ │ ├── global │ │ └── plugin_dsl.rb │ │ ├── header.rb │ │ ├── header │ │ └── attribute.rb │ │ ├── initializer.rb │ │ ├── lint │ │ ├── enumerable_dataset.rb │ │ ├── gateway.rb │ │ ├── linter.rb │ │ ├── spec.rb │ │ └── test.rb │ │ ├── mapper.rb │ │ ├── mapper │ │ ├── attribute_dsl.rb │ │ ├── builder.rb │ │ ├── dsl.rb │ │ └── model_dsl.rb │ │ ├── mapper_compiler.rb │ │ ├── mapper_registry.rb │ │ ├── memory.rb │ │ ├── memory │ │ ├── associations.rb │ │ ├── associations │ │ │ ├── many_to_many.rb │ │ │ ├── many_to_one.rb │ │ │ ├── one_to_many.rb │ │ │ └── one_to_one.rb │ │ ├── commands.rb │ │ ├── dataset.rb │ │ ├── gateway.rb │ │ ├── mapper_compiler.rb │ │ ├── relation.rb │ │ ├── schema.rb │ │ ├── storage.rb │ │ └── types.rb │ │ ├── model_builder.rb │ │ ├── open_struct.rb │ │ ├── pipeline.rb │ │ ├── plugin.rb │ │ ├── plugin_registry.rb │ │ ├── plugins.rb │ │ ├── plugins │ │ ├── command │ │ │ ├── schema.rb │ │ │ └── timestamps.rb │ │ ├── relation │ │ │ ├── instrumentation.rb │ │ │ └── registry_reader.rb │ │ └── schema │ │ │ └── timestamps.rb │ │ ├── processor.rb │ │ ├── processor │ │ └── transproc.rb │ │ ├── registry.rb │ │ ├── relation.rb │ │ ├── relation │ │ ├── class_interface.rb │ │ ├── combined.rb │ │ ├── commands.rb │ │ ├── composite.rb │ │ ├── curried.rb │ │ ├── graph.rb │ │ ├── loaded.rb │ │ ├── materializable.rb │ │ ├── name.rb │ │ ├── view_dsl.rb │ │ └── wrap.rb │ │ ├── relation_registry.rb │ │ ├── schema.rb │ │ ├── schema │ │ ├── associations_dsl.rb │ │ ├── dsl.rb │ │ └── inferrer.rb │ │ ├── schema_plugin.rb │ │ ├── setup.rb │ │ ├── setup │ │ ├── auto_registration.rb │ │ ├── auto_registration_strategies │ │ │ ├── base.rb │ │ │ ├── custom_namespace.rb │ │ │ ├── no_namespace.rb │ │ │ └── with_namespace.rb │ │ ├── finalize.rb │ │ └── finalize │ │ │ ├── finalize_commands.rb │ │ │ ├── finalize_mappers.rb │ │ │ └── finalize_relations.rb │ │ ├── struct.rb │ │ ├── struct_compiler.rb │ │ ├── support │ │ ├── configurable.rb │ │ ├── inflector.rb │ │ ├── memoizable.rb │ │ └── notifications.rb │ │ ├── transaction.rb │ │ ├── transformer.rb │ │ └── types.rb ├── log │ └── .gitkeep ├── rakelib │ ├── benchmark.rake │ └── rubocop.rake ├── rom-core.gemspec └── spec │ ├── fixtures │ ├── app │ │ ├── commands │ │ │ └── create_user.rb │ │ ├── mappers │ │ │ └── user_list.rb │ │ ├── my_commands │ │ │ └── create_user.rb │ │ ├── my_mappers │ │ │ └── user_list.rb │ │ ├── my_relations │ │ │ └── users.rb │ │ └── relations │ │ │ └── users.rb │ ├── custom │ │ ├── commands │ │ │ └── create_user.rb │ │ ├── mappers │ │ │ └── user_list.rb │ │ └── relations │ │ │ └── users.rb │ ├── custom_namespace │ │ ├── commands │ │ │ └── create_customer.rb │ │ ├── mappers │ │ │ └── customer_list.rb │ │ └── relations │ │ │ └── customers.rb │ ├── lib │ │ └── persistence │ │ │ ├── commands │ │ │ └── create_user.rb │ │ │ ├── mappers │ │ │ └── user_list.rb │ │ │ ├── my_commands │ │ │ └── create_user.rb │ │ │ ├── my_mappers │ │ │ └── user_list.rb │ │ │ ├── my_relations │ │ │ └── users.rb │ │ │ └── relations │ │ │ └── users.rb │ └── wrong │ │ ├── commands │ │ └── create_customer.rb │ │ ├── mappers │ │ └── customer_list.rb │ │ └── relations │ │ └── customers.rb │ ├── integration │ ├── command_registry_spec.rb │ ├── commands │ │ ├── create_spec.rb │ │ ├── delete_spec.rb │ │ └── update_spec.rb │ ├── commands_spec.rb │ ├── gateways │ │ ├── extending_relations_spec.rb │ │ └── setting_logger_spec.rb │ ├── mapper │ │ ├── combine_spec.rb │ │ ├── deep_embedded_spec.rb │ │ ├── definition_dsl_spec.rb │ │ ├── embedded_spec.rb │ │ ├── exclude_spec.rb │ │ ├── fold_spec.rb │ │ ├── group_spec.rb │ │ ├── mapper_spec.rb │ │ ├── overwrite_attributes_value_spec.rb │ │ ├── prefix_separator_spec.rb │ │ ├── prefix_spec.rb │ │ ├── prefixing_attributes_spec.rb │ │ ├── registering_custom_mappers_spec.rb │ │ ├── renaming_attributes_spec.rb │ │ ├── reusing_mappers_spec.rb │ │ ├── setup_spec.rb │ │ ├── step_spec.rb │ │ ├── symbolizing_attributes_spec.rb │ │ ├── unfold_spec.rb │ │ ├── ungroup_spec.rb │ │ ├── unwrap_spec.rb │ │ └── wrap_spec.rb │ ├── memory │ │ └── commands │ │ │ ├── create_spec.rb │ │ │ ├── delete_spec.rb │ │ │ └── update_spec.rb │ ├── multi_env_spec.rb │ ├── multi_repo_spec.rb │ ├── relations │ │ ├── default_dataset_spec.rb │ │ ├── inheritance_spec.rb │ │ ├── reading_spec.rb │ │ └── registry_dsl_spec.rb │ ├── setup_spec.rb │ └── transformer_spec.rb │ ├── shared │ ├── command_behavior.rb │ ├── command_graph.rb │ ├── container.rb │ ├── enumerable_dataset.rb │ ├── gateway_only.rb │ ├── materializable.rb │ ├── no_container.rb │ ├── one_behavior.rb │ └── users_and_tasks.rb │ ├── spec_helper.rb │ ├── support │ ├── constant_leak_finder.rb │ ├── schema.rb │ └── types.rb │ ├── test │ └── memory_repository_lint_test.rb │ └── unit │ └── rom │ ├── array_dataset_spec.rb │ ├── association_set_spec.rb │ ├── associations │ ├── many_to_one_spec.rb │ └── one_to_many_spec.rb │ ├── attribute │ ├── method_missing_spec.rb │ ├── optional_spec.rb │ └── to_ast_spec.rb │ ├── auto_curry_spec.rb │ ├── cache_spec.rb │ ├── command_compiler_spec.rb │ ├── commands │ ├── graph_spec.rb │ ├── lazy_spec.rb │ └── pre_and_post_processors_spec.rb │ ├── commands_spec.rb │ ├── configurable_spec.rb │ ├── configuration_spec.rb │ ├── container_spec.rb │ ├── create_container_spec.rb │ ├── enumerable_dataset_spec.rb │ ├── environment_spec.rb │ ├── gateway_spec.rb │ ├── mapper │ ├── dsl_spec.rb │ └── model_dsl_spec.rb │ ├── mapper_compiler_spec.rb │ ├── mapper_spec.rb │ ├── memoizable_spec.rb │ ├── memory │ ├── commands_spec.rb │ ├── dataset_spec.rb │ ├── gateway_spec.rb │ ├── inheritance_spec.rb │ ├── relation_spec.rb │ └── storage_spec.rb │ ├── plugin_spec.rb │ ├── plugins │ ├── command │ │ ├── schema_spec.rb │ │ └── timestamps_spec.rb │ ├── relation │ │ └── instrumentation_spec.rb │ └── schema │ │ └── timestamps_spec.rb │ ├── processor │ └── transproc_spec.rb │ ├── registry_spec.rb │ ├── relation │ ├── as_spec.rb │ ├── attribute_reader_spec.rb │ ├── call_spec.rb │ ├── class_interface │ │ ├── relation_name_spec.rb │ │ └── view_spec.rb │ ├── combine_spec.rb │ ├── combine_with_spec.rb │ ├── combined │ │ ├── call_spec.rb │ │ ├── combine_spec.rb │ │ ├── command_spec.rb │ │ ├── graph_method_missing_spec.rb │ │ ├── graph_predicate_spec.rb │ │ ├── map_to_spec.rb │ │ ├── map_with_spec.rb │ │ ├── materializable_spec.rb │ │ ├── method_missing_spec.rb │ │ ├── to_a_spec.rb │ │ ├── with_nodes_spec.rb │ │ └── wrap_spec.rb │ ├── command_spec.rb │ ├── composite_spec.rb │ ├── curried_spec.rb │ ├── eager_load_spec.rb │ ├── foreign_key_spec.rb │ ├── inspect_spec.rb │ ├── lazy_spec.rb │ ├── loaded_spec.rb │ ├── map_to_spec.rb │ ├── map_with_spec.rb │ ├── name_spec.rb │ ├── new_spec.rb │ ├── output_schema_spec.rb │ ├── preload_assoc_spec.rb │ ├── schema_spec.rb │ ├── struct_namespace_spec.rb │ ├── to_ast_spec.rb │ ├── wrap │ │ └── combine_spec.rb │ └── wrap_spec.rb │ ├── relation_spec.rb │ ├── schema │ ├── accessing_attributes_spec.rb │ ├── append_spec.rb │ ├── associations_dsl_spec.rb │ ├── attribute_spec.rb │ ├── canonical_spec.rb │ ├── exclude_spec.rb │ ├── finalize_spec.rb │ ├── key_predicate_spec.rb │ ├── merge_spec.rb │ ├── prefix_spec.rb │ ├── project_spec.rb │ ├── rename_spec.rb │ ├── uniq_spec.rb │ └── wrap_spec.rb │ ├── schema_spec.rb │ ├── setup │ └── auto_registration_spec.rb │ └── struct_compiler_spec.rb ├── docker-compose.yml ├── docker_start.sh ├── lib ├── rom.rb └── rom │ └── version.rb ├── repository ├── .rspec ├── .simplecov ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks │ ├── basic.rb │ ├── command_vs_changeset.rb │ ├── gc_suite.rb │ ├── profile_one.rb │ ├── seed.rb │ └── setup.rb ├── docsite │ └── source │ │ ├── index.html.md │ │ ├── quick-start.html.md │ │ ├── reading-aggregates.html.md │ │ ├── reading-simple-objects.html.md │ │ └── writing-aggregates.html.md ├── examples │ ├── sql.rb │ └── upsert.rb ├── lib │ ├── rom-repository.rb │ └── rom │ │ ├── repository.rb │ │ └── repository │ │ ├── class_interface.rb │ │ ├── relation_reader.rb │ │ ├── root.rb │ │ ├── session.rb │ │ └── version.rb ├── log │ └── .gitkeep ├── rom-repository.gemspec └── spec │ ├── integration │ ├── combine_spec.rb │ ├── command_macros_spec.rb │ ├── dependency_injection_spec.rb │ ├── multi_adapter_spec.rb │ ├── plugin_spec.rb │ ├── repository_spec.rb │ ├── root_repository_spec.rb │ └── typed_structs_spec.rb │ ├── shared │ ├── database.rb │ ├── mappers.rb │ ├── models.rb │ ├── plugins.rb │ ├── relations.rb │ ├── repo.rb │ ├── seeds.rb │ └── structs.rb │ ├── spec_helper.rb │ ├── support │ └── mapper_registry.rb │ └── unit │ ├── repository │ ├── class_interface │ │ └── root_builder_spec.rb │ ├── inspect_spec.rb │ ├── session_spec.rb │ └── transaction_spec.rb │ └── session_spec.rb ├── rom.gemspec └── spec ├── integration ├── gateways │ └── gateway_params_spec.rb └── setup_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # this file is managed by rom-rb/devtools project 2 | 3 | version: "2" 4 | 5 | exclude_patterns: 6 | - "benchmarks/" 7 | - "examples/" 8 | - "spec/" 9 | 10 | plugins: 11 | rubocop: 12 | enabled: true 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | Dockerfile 3 | docker-compose.yml 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [solnic, timriley] 2 | open_collective: rom 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discourse.dry-rb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the dry-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.dry-rb.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: 4 | -------------------------------------------------------------------------------- /.github/workflows/custom/ci.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | tests: 3 | services: 4 | db: 5 | image: postgres:16.2 6 | env: 7 | POSTGRES_USER: runner 8 | POSTGRES_PASSWORD: "" 9 | POSTGRES_DB: rom_repository 10 | POSTGRES_HOST_AUTH_METHOD: trust 11 | ports: 12 | - 5432:5432 13 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 14 | strategy: 15 | matrix: 16 | ruby: 17 | - "3.3" 18 | - "3.2" 19 | - "3.1" 20 | - "3.0" 21 | include: 22 | - ruby: "3.3" 23 | coverage: "true" 24 | use_transproc_master: "true" 25 | use_use_rom_sql_master: "true" 26 | env: 27 | COVERAGE: ${{matrix.coverage}} 28 | BASE_DB_URI: localhost/rom_repository 29 | USE_TRANSPROC_MASTER: ${{matrix.use_dry_transformer_master}} 30 | USE_ROM_SQL_MASTER: ${{matrix.use_rom_sql_master}} 31 | CODACY_RUN_LOCAL: ${{matrix.coverage}} 32 | CODACY_PROJECT_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 33 | APT_DEPS: "libpq-dev libsqlite3-dev" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | Gemfile.lock 15 | vendor/ 16 | log/* 17 | **/log/* 18 | 19 | .idea/ 20 | .vscode/ 21 | 22 | # YARD artifacts 23 | .yardoc 24 | _yardoc 25 | doc/ 26 | .ruby-version 27 | */.ruby-version 28 | */Gemfile.lock 29 | */doc 30 | -------------------------------------------------------------------------------- /.inch.yml: -------------------------------------------------------------------------------- 1 | files: 2 | included: 3 | - core/lib/**/*.rb 4 | - mapper/lib/**/*.rb 5 | - repository/lib/**/*.rb 6 | - changeset/lib/**/*.rb 7 | excluded: 8 | - lib/**/*.rb 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | 5 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SimpleCov.command_name "spec:#{SPEC_ROOT.join('..').basename}" 4 | 5 | SimpleCov.root(SPEC_ROOT.join('../..').to_s) 6 | 7 | SimpleCov.start do 8 | add_filter '/spec/' 9 | add_filter '/rom-sql/' 10 | add_filter '/lint/' 11 | end 12 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --query '@api.text != "private"' 2 | --embed-mixins 3 | -o doc 4 | -r README.md 5 | --files CHANGELOG.md 6 | --markup-provider=redcarpet 7 | --markup=markdown 8 | --plugin junk 9 | core/lib/**/*.rb 10 | changeset/lib/**/*.rb 11 | mapper/lib/**/*.rb 12 | repository/lib/**/*.rb 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct) 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:latest 2 | 3 | # Install needed dependencies 4 | RUN apt-get update -qq && apt-get install -y libsqlite3-dev 5 | 6 | # Some files & directories variables 7 | ENV rom /opt/rom/ 8 | ENV bundle_path /opt/box 9 | 10 | # Create non-root user 11 | RUN useradd -m user && \ 12 | # Create rom directory 13 | mkdir $rom && \ 14 | # Set user as owner of rom directory 15 | chown user:user $rom 16 | 17 | # Path for bundles 18 | ENV BUNDLE_PATH $bundle_path 19 | RUN mkdir $bundle_path && chown user:user $bundle_path 20 | 21 | # Work as non-root in rom directory 22 | USER user 23 | WORKDIR $rom 24 | 25 | # Add the code 26 | COPY --chown=user:user . $rom -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | gem "rake", ">= 13.0.0" 6 | 7 | group :test do 8 | gem "simplecov", require: false, platforms: :ruby 9 | gem "simplecov-cobertura", require: false, platforms: :ruby 10 | gem "rexml", require: false 11 | gem "tempfile" 12 | 13 | gem "warning" 14 | end 15 | 16 | group :tools do 17 | # this is the same version that we use on codacy 18 | gem "rubocop", "1.69.2" 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 rom-rb team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | spec_results = {} 6 | 7 | desc 'Run all specs' 8 | task :spec do 9 | %w[core repository changeset rom].map do |name| 10 | Rake::Task["spec:#{name}"].execute 11 | end 12 | 13 | if spec_results.values.any? { |v| v.equal?(false) } 14 | abort("\nspecs failed\n") 15 | end 16 | end 17 | 18 | namespace :spec do 19 | desc 'Run rom specs' 20 | task :rom do 21 | spec_results[:rom] = system('bundle exec rspec spec/**/*_spec.rb') 22 | end 23 | 24 | desc 'Run core specs' 25 | task :core do 26 | spec_results[:core] = system('cd core && bundle exec rake spec') 27 | end 28 | 29 | desc 'Run repository specs' 30 | task :repository do 31 | spec_results[:repository] = system('cd repository && bundle exec rake spec') 32 | end 33 | 34 | desc 'Run changeset specs' 35 | task :changeset do 36 | spec_results[:changeset] = system('cd changeset && bundle exec rake spec') 37 | end 38 | end 39 | 40 | task default: :spec 41 | 42 | begin 43 | require 'yard-junk/rake' 44 | YardJunk::Rake.define_task(:text) 45 | rescue LoadError 46 | # ignore 47 | end 48 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for p in core repository changeset; do 4 | cd $p && bundle install --without benchmarks console tools --gemfile ./Gemfile && cd .. 5 | done 6 | 7 | bundle install --without benchmarks console tools --gemfile ./Gemfile 8 | -------------------------------------------------------------------------------- /bin/doc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for p in core mapper repository 4 | do 5 | cd $p && yard && cd .. 6 | done -------------------------------------------------------------------------------- /changeset/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | --require ./spec/spec_helper 4 | -------------------------------------------------------------------------------- /changeset/.yardopts: -------------------------------------------------------------------------------- 1 | --query '@api.text != "private"' 2 | --embed-mixins 3 | -o ../doc/rom-repository 4 | -r README.md 5 | --markup-provider=redcarpet 6 | --markup=markdown 7 | --files CHANGELOG.md 8 | -------------------------------------------------------------------------------- /changeset/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.2 2018-03-29 2 | 3 | ### Fixed 4 | 5 | * `add_timestamps` mapping no longer overwrites existing timestamps (cgeorgii) 6 | 7 | # 1.0.1 2017-11-02 8 | 9 | ### Fixed 10 | 11 | * Depends on final release of `rom-core ~> 4.0` (solnic) 12 | 13 | # 1.0.0 2017-10-18 14 | 15 | rom-changeset was extracted from rom-repository 16 | 17 | ### Added 18 | 19 | - `#changeset` interfaced was ported to a relation plugin and now `Relation#changeset` is available (solnic) 20 | 21 | ### Changed 22 | 23 | - Changesets are no longer coupled to repositories (solnic) 24 | - Changesets use relations to retrieve their commands (solnic) 25 | -------------------------------------------------------------------------------- /changeset/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | COMPONENTS = %w[core repository].freeze 4 | 5 | eval_gemfile '../Gemfile' 6 | -------------------------------------------------------------------------------- /changeset/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 rom-rb team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /changeset/README.md: -------------------------------------------------------------------------------- 1 | [gem]: https://rubygems.org/gems/rom-changeset 2 | 3 | # rom-changeset 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rom-changeset.svg)][gem] 6 | 7 | Changesets for [rom-rb](https://github.com/rom-rb/rom) integrated with relations. 8 | 9 | Resources: 10 | 11 | * [User documentation](http://rom-rb.org/4.0/learn/core/changesets/) 12 | * [API documentation](http://api.rom-rb.org/rom/) 13 | 14 | ## License 15 | 16 | See `LICENSE` file. 17 | -------------------------------------------------------------------------------- /changeset/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rake/testtask' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | task default: [:ci] 9 | 10 | desc 'Run specs in isolation' 11 | task :'spec:isolation' do 12 | FileList['spec/**/*_spec.rb'].each do |spec| 13 | sh 'rspec', spec 14 | end 15 | end 16 | 17 | desc 'Run CI tasks' 18 | task ci: [:spec] 19 | -------------------------------------------------------------------------------- /changeset/docsite/source/associations.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: 3 3 | chapter: Changeset 4 | title: Associations 5 | --- 6 | 7 | Changesets can be associated with each other using `Changeset#associate` method, which will automatically set foreign keys for you, based on schema associations. Let's define `:users` relation that has many `:tasks`: 8 | 9 | ``` ruby 10 | class Users < ROM::Relation[:sql] 11 | schema(infer: true) do 12 | associations do 13 | has_many :tasks 14 | end 15 | end 16 | end 17 | 18 | class Tasks < ROM::Relation[:sql] 19 | schema(infer: true) do 20 | associations do 21 | belongs_to :user 22 | end 23 | end 24 | end 25 | ``` 26 | 27 | With associations established in the schema, we can easily associate data using changesets and commit them in a transaction: 28 | 29 | ``` ruby 30 | task = tasks.transaction do 31 | user = users.changeset(:create, name: 'Jane').commit 32 | 33 | new_task = tasks.changeset(:create, title: 'Task One').associate(user) 34 | 35 | new_task.commit 36 | end 37 | 38 | task 39 | # {:id=>1, :user_id=>1, :title=>"Task One"} 40 | ``` 41 | 42 | ^INFO 43 | #### Association name 44 | 45 | Notice that `associate` method can accept a rom struct and it will try to infer association name from it. If this fails because you have an aliased association then pass association name explicitly as the second argument, ie: `associate(user, :author)`. 46 | ^ 47 | -------------------------------------------------------------------------------- /changeset/lib/rom-changeset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/changeset' 4 | require 'rom/plugins/relation/changeset' 5 | -------------------------------------------------------------------------------- /changeset/lib/rom/changeset/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Changeset 5 | # Changeset specialization for create commands 6 | # 7 | # @see Changeset::Stateful 8 | # 9 | # @api public 10 | class Create < Stateful 11 | command_type :create 12 | 13 | def command 14 | super.new(relation) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /changeset/lib/rom/changeset/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Changeset 5 | # Changeset specialization for delete commands 6 | # 7 | # Delete changesets will execute delete command for its relation, which 8 | # means proper restricted relations should be used with this changeset. 9 | # 10 | # @api public 11 | class Delete < Changeset 12 | command_type :delete 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /changeset/lib/rom/changeset/extensions/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/graph' 4 | 5 | module ROM 6 | class Changeset 7 | # Namespace for changeset extensions 8 | # 9 | # @api public 10 | module Extensions 11 | # Changeset extenions for combined relations 12 | # 13 | # @api public 14 | module Relation 15 | class Graph 16 | # Build a changeset for a combined relation 17 | # 18 | # @raise NotImplementedError 19 | # 20 | # @api public 21 | def changeset(*) 22 | raise NotImplementedError, "Changeset doesn't support combined relations yet" 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /changeset/lib/rom/changeset/pipe_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'transproc/all' 4 | require 'transproc/registry' 5 | 6 | module ROM 7 | class Changeset 8 | # Transproc Registry useful for pipe 9 | # 10 | # @api private 11 | module PipeRegistry 12 | extend Transproc::Registry 13 | 14 | import Transproc::Coercions 15 | import Transproc::HashTransformations 16 | 17 | def self.add_timestamps(data) 18 | now = Time.now 19 | Hash(created_at: now, updated_at: now).merge(data) 20 | end 21 | 22 | def self.touch(data) 23 | Hash(updated_at: Time.now).merge(data) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /changeset/lib/rom/changeset/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Changeset 5 | VERSION = '5.4.2' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /changeset/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rom-rb/rom/d2de00f6249d17aea7965573972633677018f4cf/changeset/log/.gitkeep -------------------------------------------------------------------------------- /changeset/rom-changeset.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/rom/changeset/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'rom-changeset' 7 | gem.summary = 'Changeset abstraction for rom-rb' 8 | gem.description = 'rom-changeset adds support for preprocessing data on ' \ 9 | 'top of rom-rb repositories' 10 | gem.author = 'Piotr Solnica' 11 | gem.email = 'piotr.solnica+oss@gmail.com' 12 | gem.homepage = 'http://rom-rb.org' 13 | gem.require_paths = ['lib'] 14 | gem.version = ROM::Changeset::VERSION.dup 15 | gem.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'lib/**/*'] 16 | gem.license = 'MIT' 17 | gem.metadata = { 18 | 'source_code_uri' => 'https://github.com/rom-rb/rom/tree/master/changeset', 19 | 'documentation_uri' => 'https://api.rom-rb.org/rom/', 20 | 'mailing_list_uri' => 'https://discourse.rom-rb.org/', 21 | 'bug_tracker_uri' => 'https://github.com/rom-rb/rom/issues', 22 | 'rubygems_mfa_required' => 'true' 23 | } 24 | 25 | gem.add_dependency 'dry-core', '~> 1.0' 26 | gem.add_dependency 'rom-core', '~> 5.4' 27 | gem.add_dependency 'transproc', '~> 1.1' 28 | end 29 | -------------------------------------------------------------------------------- /changeset/spec/integration/command_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'changeset plugin' do 4 | subject(:changeset) do 5 | books.changeset(changeset_class).data(title: 'Hello World') 6 | end 7 | 8 | let(:changeset_class) do 9 | Class.new(ROM::Changeset::Create) do 10 | command_options input: -> tuple { tuple.merge(updated_at: Time.now) } 11 | command_plugins timestamps: { timestamps: [:created_at] } 12 | end 13 | end 14 | 15 | include_context 'database' 16 | include_context 'relations' 17 | 18 | it 'extends the command with the provided command options' do 19 | book = changeset.commit 20 | 21 | expect(book.created_at).to be_within(0.25).of(Time.now) 22 | expect(book.updated_at).to be_within(0.25).of(Time.now) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /changeset/spec/integration/plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'changeset plugin' do 4 | subject(:changeset) do 5 | books.changeset(changeset_class).data(title: 'Hello World') 6 | end 7 | 8 | let(:changeset_class) do 9 | Class.new(ROM::Changeset::Create) do 10 | use :auto_touch 11 | end 12 | end 13 | 14 | include_context 'database' 15 | include_context 'relations' 16 | 17 | before do 18 | module Test 19 | module AutoTouch 20 | def self.apply(target, **) 21 | target.map { add_timestamps } 22 | end 23 | end 24 | end 25 | 26 | ROM.plugins do 27 | register :auto_touch, Test::AutoTouch, type: :changeset 28 | end 29 | end 30 | 31 | it 'extends changeset with the functionality provided by the plugin' do 32 | book = changeset.commit 33 | 34 | expect(book.created_at).to be_within(0.25).of(Time.now) 35 | expect(book.updated_at).to be_within(0.25).of(Time.now) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /changeset/spec/shared/mappers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'mappers' do 4 | let(:user_mappers) { users.mappers[:user] } 5 | let(:task_mappers) { tasks.mappers[:task] } 6 | let(:tag_mappers) { tags.mappers[:tag] } 7 | 8 | before do 9 | configuration.mappers do 10 | define(:users) do 11 | model Test::Models::User 12 | register_as :user 13 | 14 | attribute :id 15 | attribute :name 16 | end 17 | 18 | define(:tasks) do 19 | model Test::Models::Task 20 | register_as :task 21 | 22 | attribute :id 23 | attribute :user_id 24 | attribute :title 25 | end 26 | 27 | define(:tags) do 28 | model Test::Models::Tag 29 | register_as :tag 30 | 31 | attribute :id 32 | attribute :task_id 33 | attribute :name 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /changeset/spec/shared/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'models' do 4 | let(:user_model) { Test::Models::User } 5 | let(:task_model) { Test::Models::Task } 6 | let(:tag_model) { Test::Models::Tag } 7 | 8 | before do 9 | module Test 10 | module Models 11 | class User 12 | include Dry::Equalizer(:id, :name) 13 | 14 | attr_reader :id 15 | 16 | attr_reader :name 17 | 18 | def initialize(attrs) 19 | @id = attrs[:id] 20 | @name = attrs[:name] 21 | end 22 | end 23 | 24 | class Task 25 | include Dry::Equalizer(:id, :user_id, :title) 26 | 27 | attr_reader :id 28 | 29 | attr_reader :user_id 30 | 31 | attr_reader :title 32 | 33 | def initialize(attrs) 34 | @id = attrs[:id] 35 | @name = attrs[:name] 36 | @title = attrs[:title] 37 | end 38 | end 39 | 40 | class Tag 41 | include Dry::Equalizer(:id, :task_id, :name) 42 | 43 | attr_reader :id 44 | attr_reader :task_id 45 | attr_reader :name 46 | 47 | def initialize(attrs) 48 | @id = attrs[:id] 49 | @task_id = attrs[:task_id] 50 | @name = attrs[:name] 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /changeset/spec/shared/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'seeds' do 4 | before do 5 | jane_id = conn[:users].insert name: 'Jane' 6 | joe_id = conn[:users].insert name: 'Joe' 7 | 8 | conn[:tasks].insert user_id: joe_id, title: 'Joe Task' 9 | task_id = conn[:tasks].insert user_id: jane_id, title: 'Jane Task' 10 | 11 | conn[:tags].insert task_id: task_id, name: 'red' 12 | 13 | jane_post_id = conn[:posts].insert( 14 | author_id: jane_id, 15 | title: 'Hello From Jane', 16 | body: 'Jane Post' 17 | ) 18 | joe_post_id = conn[:posts].insert(author_id: joe_id, title: 'Hello From Joe', body: 'Joe Post') 19 | 20 | red_id = conn[:labels].insert name: 'red' 21 | green_id = conn[:labels].insert name: 'green' 22 | blue_id = conn[:labels].insert name: 'blue' 23 | 24 | conn[:posts_labels].insert post_id: jane_post_id, label_id: red_id 25 | conn[:posts_labels].insert post_id: jane_post_id, label_id: blue_id 26 | 27 | conn[:posts_labels].insert post_id: joe_post_id, label_id: green_id 28 | 29 | conn[:messages].insert author: 'Jane', body: 'Hello folks' 30 | conn[:messages].insert author: 'Joe', body: 'Hello Jane' 31 | 32 | conn[:reactions].insert message_id: 1, author: 'Joe' 33 | conn[:reactions].insert message_id: 1, author: 'Anonymous' 34 | conn[:reactions].insert message_id: 2, author: 'Jane' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /changeset/spec/unit/create_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Changeset::Create do 4 | include_context 'database' 5 | include_context 'relations' 6 | 7 | context 'with a hash' do 8 | let(:changeset) do 9 | users.changeset(:create, name: 'Jane') 10 | end 11 | 12 | it 'has data' do 13 | expect(changeset.to_h).to eql(name: 'Jane') 14 | end 15 | 16 | it 'has relation' do 17 | expect(changeset.relation).to be(users) 18 | end 19 | 20 | it 'can be commited' do 21 | expect(changeset.commit.to_h).to eql(id: 1, name: 'Jane') 22 | end 23 | end 24 | 25 | context 'with an array' do 26 | let(:changeset) do 27 | users.changeset(:create, data) 28 | end 29 | 30 | let(:data) do 31 | [{ name: 'Jane' }, { name: 'Joe' }] 32 | end 33 | 34 | it 'has data' do 35 | expect(changeset.to_a).to eql(data) 36 | end 37 | 38 | it 'has relation' do 39 | expect(changeset.relation).to be(users) 40 | end 41 | 42 | it 'can be commited' do 43 | expect(changeset.commit.map(&:to_h)).to eql([{ id: 1, name: 'Jane' }, { id: 2, name: 'Joe' }]) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /changeset/spec/unit/update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Changeset::Update do 4 | include_context 'database' 5 | include_context 'relations' 6 | 7 | subject(:changeset) do 8 | users.by_pk(jane[:id]).changeset(:update, data) 9 | end 10 | 11 | let(:data) do 12 | { name: 'Jane Doe' } 13 | end 14 | 15 | let!(:jane) do 16 | users.command(:create).call(name: 'Jane') 17 | end 18 | 19 | let!(:joe) do 20 | users.command(:create).call(name: 'Joe') 21 | end 22 | 23 | it 'has data' do 24 | expect(changeset.to_h).to eql(name: 'Jane Doe') 25 | end 26 | 27 | it 'has diff' do 28 | expect(changeset.diff).to eql(name: 'Jane Doe') 29 | end 30 | 31 | it 'has relation' do 32 | expect(changeset.relation.one).to eql(users.by_pk(jane[:id]).one) 33 | end 34 | 35 | it 'can be commited' do 36 | expect(changeset.commit.to_h).to eql(id: 1, name: 'Jane Doe') 37 | expect(users.by_pk(joe[:id]).one).to eql(joe) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /core/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | --require ./spec/spec_helper.rb 4 | -------------------------------------------------------------------------------- /core/.simplecov: -------------------------------------------------------------------------------- 1 | ../.simplecov -------------------------------------------------------------------------------- /core/.yardopts: -------------------------------------------------------------------------------- 1 | --plugin junk 2 | --query '@api.text != "private"' 3 | --embed-mixins 4 | -o ../doc/rom-core 5 | -r README.md 6 | --markup-provider=redcarpet 7 | --markup=markdown 8 | --files CHANGELOG.md 9 | -------------------------------------------------------------------------------- /core/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | -------------------------------------------------------------------------------- /core/Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | group :red_green_refactor, halt_on_fail: true do 4 | guard :rspec, cmd: 'rspec', all_on_start: true do 5 | # run all specs if Gemfile.lock is modified 6 | watch('Gemfile.lock') { 'spec' } 7 | 8 | # run all specs if any library code is modified 9 | watch(%r{\Alib/.+\.rb\z}) { 'spec' } 10 | 11 | # run all specs if supporting files are modified 12 | watch('spec/spec_helper.rb') { 'spec' } 13 | watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec' } 14 | 15 | # run a spec if it is modified 16 | watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z}) 17 | 18 | notification :tmux, display_message: true if ENV.key?('TMUX') 19 | end 20 | 21 | guard :rubocop do 22 | # run rubocop on modified file 23 | watch(%r{\Alib/.+\.rb\z}) 24 | watch(%r{\Aspec/.+\.rb\z}) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /core/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 rom-rb team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | [gem]: https://rubygems.org/gems/rom-core 2 | 3 | # rom-core [![Gitter chat](https://badges.gitter.im/rom-rb/chat.svg)](https://gitter.im/rom-rb/chat) 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rom-core.svg)][gem] 6 | 7 | Core API for rom-rb and its adapters and extensions. 8 | 9 | Resources: 10 | 11 | * [User documentation](http://rom-rb.org/learn/core) 12 | * [API documentation](http://api.rom-rb.org/rom/) 13 | 14 | ## License 15 | 16 | See `LICENSE` file. 17 | -------------------------------------------------------------------------------- /core/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rake/testtask' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | task default: [:ci] 9 | 10 | desc 'Run specs in isolation' 11 | task :'spec:isolation' do 12 | FileList['spec/**/*_spec.rb'].each do |spec| 13 | sh "COVERAGE=false bundle exec rspec #{spec}" 14 | end 15 | end 16 | 17 | desc 'Run CI tasks' 18 | if RUBY_ENGINE == 'ruby' 19 | task ci: %i[spec lint spec:isolation] 20 | else 21 | task ci: %i[spec lint] 22 | end 23 | 24 | Rake::TestTask.new(:lint) do |test| 25 | test.description = 'Run adapter lint tests against memory adapter' 26 | test.test_files = FileList.new('spec/test/*_test.rb') 27 | test.libs << 'test' 28 | test.verbose = true 29 | end 30 | -------------------------------------------------------------------------------- /core/benchmarks/gc_suite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Enable and start GC before each job run. Disable GC afterwards. 4 | # 5 | # Inspired by https://www.omniref.com/ruby/2.2.1/symbols/Benchmark/bm?#annotation=4095926&line=182 6 | class GCSuite 7 | def warming(*) 8 | run_gc 9 | end 10 | 11 | def running(*) 12 | run_gc 13 | end 14 | 15 | def warmup_stats(*); end 16 | 17 | def add_report(*); end 18 | 19 | private 20 | 21 | def run_gc 22 | GC.enable 23 | GC.start 24 | GC.disable 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /core/benchmarks/profile_combine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'setup' 4 | require 'hotch' 5 | 6 | Hotch() do 7 | 100.times do 8 | users.combine(:tasks).to_a 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/benchmarks/profile_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'setup' 4 | require 'hotch' 5 | 6 | users.by_name('User 1').one 7 | 8 | Hotch() do 9 | 1000.times do 10 | users.by_name('User 1').one 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/benchmarks/profile_wrap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'setup' 4 | require 'hotch' 5 | 6 | rel = tasks.limit(100).wrap(:user) 7 | 8 | rel.to_a 9 | 10 | Hotch() do 11 | 1000.times do 12 | rel.each { |t| t.user.name } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: 3 3 | chapter: Core 4 | sections: 5 | - quick-setup 6 | - relations 7 | - schemas 8 | - associations 9 | - combines 10 | - commands 11 | - structs 12 | - mappers 13 | - framework-setup 14 | --- 15 | 16 | In this section you can learn about APIs that are common for all adapters. Once you understand basic concepts, you can check out [Repositories](/learn/repository/%{version}) and [Changeset](/learn/changeset/%{version}) sections. 17 | 18 | - [Quick setup](/learn/core/%{version}/quick-setup) explains how to quickly set up ROM 19 | - [Relations](/learn/core/%{version}/relations) explains the purpose and basic usage of relations 20 | - [Schemas](/learn/core/%{version}/schemas) explains how you can leverage relation schemas 21 | - [Associations](/learn/core/%{version}/associations) explains how associations work 22 | - [Combines](/learn/core/%{version}/combines) explains how to combine relations 23 | - [Commands](/learn/core/%{version}/commands) explains the purpose and basic usage of commands 24 | - [Structs](/learn/core/%{version}/structs) explains the purpose and basic usage of structs 25 | - [Mappers](/learn/core/%{version}/mappers) explains the purpose and basic usage of mappers 26 | - [Framework setup](/learn/core/%{version}/framework-setup) explains how to manually set up ROM components 27 | -------------------------------------------------------------------------------- /core/lib/rom-core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/core' 4 | -------------------------------------------------------------------------------- /core/lib/rom/array_dataset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/initializer' 4 | require 'rom/enumerable_dataset' 5 | 6 | module ROM 7 | # A helper module that adds data-proxy behavior to an array-like object 8 | # 9 | # @see EnumerableDataset 10 | # 11 | # @api public 12 | module ArrayDataset 13 | extend DataProxy::ClassMethods 14 | include EnumerableDataset 15 | 16 | # Extends the class with data-proxy behavior 17 | # 18 | # @api private 19 | def self.included(klass) 20 | klass.class_eval do 21 | extend Initializer 22 | include DataProxy 23 | 24 | param :data 25 | end 26 | end 27 | 28 | forward( 29 | :*, :+, :-, :compact, :compact!, :flatten, :flatten!, :length, :pop, 30 | :reverse, :reverse!, :sample, :size, :shift, :shuffle, :shuffle!, 31 | :slice, :slice!, :sort!, :uniq, :uniq!, :unshift, :values_at 32 | ) 33 | 34 | %i[ 35 | map! combination cycle delete_if keep_if permutation reject! 36 | select! sort_by! 37 | ].each do |method| 38 | class_eval(<<-RUBY, __FILE__, __LINE__ + 1) 39 | def #{method}(*args, &) # def map!(*args, &) 40 | return to_enum unless block_given? # return to_enum unless block_given? 41 | self.class.new(data.__send__(:#{method}, *args, &), **options) # self.class.new(data.__send__(:map!, *args, &), **options) 42 | end # end 43 | RUBY 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /core/lib/rom/association_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/registry' 4 | 5 | module ROM 6 | # Association set contains a registry with associations defined 7 | # in schema DSL 8 | # 9 | # @api public 10 | class AssociationSet < ROM::Registry 11 | # @api private 12 | def initialize(...) 13 | super 14 | elements.values.each do |assoc| 15 | elements[assoc.name] = assoc if assoc.aliased? && !key?(assoc.name) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/definitions/many_to_many' 4 | require 'rom/associations/definitions/many_to_one' 5 | require 'rom/associations/definitions/one_to_many' 6 | require 'rom/associations/definitions/one_to_one' 7 | require 'rom/associations/definitions/one_to_one_through' 8 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions/many_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | require 'rom/associations/definitions/abstract' 5 | 6 | module ROM 7 | module Associations 8 | module Definitions 9 | # @api private 10 | class ManyToMany < Abstract 11 | result :many 12 | 13 | # @!attribute [r] through 14 | # @return [ThroughIdentifier] The name of the "through" relation 15 | option :through, reader: true 16 | 17 | # @api private 18 | def through_relation 19 | through.relation 20 | end 21 | 22 | # @api private 23 | def through_assoc_name 24 | through.assoc_name 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions/many_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/definitions/abstract' 4 | 5 | module ROM 6 | module Associations 7 | module Definitions 8 | # @api private 9 | class ManyToOne < Abstract 10 | result :one 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions/one_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/definitions/abstract' 4 | 5 | module ROM 6 | module Associations 7 | module Definitions 8 | # @api private 9 | class OneToMany < Abstract 10 | result :many 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions/one_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/definitions/abstract' 4 | 5 | module ROM 6 | module Associations 7 | module Definitions 8 | # @api private 9 | class OneToOne < Abstract 10 | result :one 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/lib/rom/associations/definitions/one_to_one_through.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/definitions/abstract' 4 | 5 | module ROM 6 | module Associations 7 | module Definitions 8 | # @api private 9 | class OneToOneThrough < ManyToMany 10 | result :one 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/lib/rom/associations/many_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/abstract' 4 | 5 | module ROM 6 | module Associations 7 | # Abstract many-to-one association type 8 | # 9 | # @api public 10 | class ManyToOne < Abstract 11 | # Adapters must implement this method 12 | # 13 | # @abstract 14 | # 15 | # @api public 16 | def call(*) 17 | raise NotImplementedError 18 | end 19 | 20 | # Return configured or inferred FK name 21 | # 22 | # @return [Symbol] 23 | # 24 | # @api public 25 | def foreign_key 26 | definition.foreign_key || source.foreign_key(target.name) 27 | end 28 | 29 | # Associate child with a parent 30 | # 31 | # @param [Hash] child The child tuple 32 | # @param [Hash] parent The parent tuple 33 | # 34 | # @return [Hash] 35 | # 36 | # @api private 37 | def associate(child, parent) 38 | fk, pk = join_key_map 39 | child.merge(fk => parent.fetch(pk)) 40 | end 41 | 42 | protected 43 | 44 | # Return foreign key on the source side 45 | # 46 | # @return [Symbol] 47 | # 48 | # @api protected 49 | def source_key 50 | foreign_key 51 | end 52 | 53 | # Return primary key on the target side 54 | # 55 | # @return [Symbol] 56 | # 57 | # @api protected 58 | def target_key 59 | target.schema.primary_key_name 60 | end 61 | 62 | memoize :foreign_key 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /core/lib/rom/associations/one_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/abstract' 4 | 5 | module ROM 6 | module Associations 7 | # Abstract one-to-many association 8 | # 9 | # @api public 10 | class OneToMany < Abstract 11 | # Adapters must implement this method 12 | # 13 | # @abstract 14 | # 15 | # @api public 16 | def call(*) 17 | raise NotImplementedError 18 | end 19 | 20 | # Return configured or inferred FK name 21 | # 22 | # @return [Symbol] 23 | # 24 | # @api public 25 | def foreign_key 26 | definition.foreign_key || target.foreign_key(source.name) 27 | end 28 | 29 | # Associate child tuple with a parent 30 | # 31 | # @param [Hash] child The child tuple 32 | # @param [Hash] parent The parent tuple 33 | # 34 | # @return [Hash] 35 | # 36 | # @api private 37 | def associate(child, parent) 38 | pk, fk = join_key_map 39 | child.merge(fk => parent.fetch(pk)) 40 | end 41 | 42 | protected 43 | 44 | # Return primary key on the source side 45 | # 46 | # @return [Symbol] 47 | # 48 | # @api protected 49 | def source_key 50 | source.schema.primary_key_name 51 | end 52 | 53 | # Return foreign key name on the target side 54 | # 55 | # @return [Symbol] 56 | # 57 | # @api protected 58 | def target_key 59 | foreign_key 60 | end 61 | 62 | memoize :foreign_key 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /core/lib/rom/associations/one_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/one_to_many' 4 | 5 | module ROM 6 | module Associations 7 | # Abstract one-to-one association type 8 | # 9 | # @api public 10 | class OneToOne < OneToMany 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /core/lib/rom/associations/one_to_one_through.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/many_to_many' 4 | 5 | module ROM 6 | module Associations 7 | # Abstract one-to-one-through association type 8 | # 9 | # @api public 10 | class OneToOneThrough < ManyToMany 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /core/lib/rom/associations/through_identifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/inflector' 4 | 5 | module ROM 6 | module Associations 7 | # @api private 8 | class ThroughIdentifier 9 | # @api private 10 | attr_reader :source 11 | 12 | # @api private 13 | attr_reader :target 14 | 15 | # @api private 16 | attr_reader :assoc_name 17 | 18 | # @api private 19 | def self.[](source, target, assoc_name = nil) 20 | new(source, target, assoc_name || default_assoc_name(target)) 21 | end 22 | 23 | # @api private 24 | def self.default_assoc_name(relation) 25 | Inflector.singularize(relation).to_sym 26 | end 27 | 28 | # @api private 29 | def initialize(source, target, assoc_name) 30 | @source = source 31 | @target = target 32 | @assoc_name = assoc_name 33 | end 34 | 35 | # @api private 36 | def to_sym 37 | source 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /core/lib/rom/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'concurrent/map' 4 | 5 | module ROM 6 | # Thread-safe cache used by various rom components 7 | # 8 | # @api private 9 | class Cache 10 | attr_reader :objects 11 | 12 | # @api private 13 | class Namespaced 14 | # @api private 15 | attr_reader :cache 16 | 17 | # @api private 18 | attr_reader :namespace 19 | 20 | # @api private 21 | def initialize(cache, namespace) 22 | @cache = cache 23 | @namespace = namespace.to_sym 24 | end 25 | 26 | # @api private 27 | def [](key) = cache[[namespace, key].hash] 28 | 29 | # @api private 30 | def fetch_or_store(*args, &) 31 | cache.fetch_or_store([namespace, args].hash, &) 32 | end 33 | 34 | # @api private 35 | def size = cache.size 36 | 37 | # @api private 38 | def inspect = %(#<#{self.class} size=#{size}>) 39 | end 40 | 41 | # @api private 42 | def initialize 43 | @objects = ::Concurrent::Map.new 44 | @namespaced = {} 45 | end 46 | 47 | def [](key) = objects[key] 48 | 49 | # @api private 50 | def fetch_or_store(*args, &) = objects.fetch_or_store(args.hash, &) 51 | 52 | # @api private 53 | def size = objects.size 54 | 55 | # @api private 56 | def namespaced(namespace) 57 | @namespaced[namespace] ||= Namespaced.new(objects, namespace) 58 | end 59 | 60 | # @api private 61 | def inspect 62 | %(#<#{self.class} size=#{size} namespaced=#{@namespaced.inspect}>) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /core/lib/rom/command_proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/inflector' 4 | 5 | module ROM 6 | # TODO: look into making command graphs work without the root key in the input 7 | # so that we can get rid of this wrapper 8 | # 9 | # @api private 10 | class CommandProxy 11 | attr_reader :command 12 | 13 | attr_reader :root 14 | 15 | # @api private 16 | def initialize(command, root = Inflector.singularize(command.name.relation).to_sym) 17 | @command = command 18 | @root = root 19 | end 20 | 21 | # @api private 22 | def call(input) = command.call(root => input) 23 | 24 | # @api private 25 | def >>(other) = self.class.new(command >> other) 26 | 27 | # @api private 28 | def restrictible? = command.restrictible? 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /core/lib/rom/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/commands/create' 4 | require 'rom/commands/update' 5 | require 'rom/commands/delete' 6 | -------------------------------------------------------------------------------- /core/lib/rom/commands/composite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/pipeline' 4 | 5 | module ROM 6 | module Commands 7 | # Composite command that consists of left and right commands 8 | # 9 | # @api public 10 | class Composite < Pipeline::Composite 11 | # Calls the composite command 12 | # 13 | # Right command is called with a result from the left one 14 | # 15 | # @return [Object] 16 | # 17 | # @api public 18 | # 19 | # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 20 | def call(*args) 21 | response = left.call(*args) 22 | 23 | if response.nil? || (many? && response.empty?) 24 | return one? ? nil : EMPTY_ARRAY 25 | end 26 | 27 | if one? && !graph? 28 | if right.is_a?(Command) || right.is_a?(Commands::Composite) 29 | right.call([response].first) 30 | else 31 | right.call([response]).first 32 | end 33 | elsif one? && graph? 34 | right.call(response).first 35 | else 36 | right.call(response) 37 | end 38 | end 39 | alias_method :[], :call 40 | # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 41 | 42 | # @api private 43 | def graph? 44 | left.is_a?(Graph) 45 | end 46 | 47 | # @api private 48 | def result 49 | left.result 50 | end 51 | 52 | # @api private 53 | def decorate?(response) 54 | super || response.is_a?(Graph) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /core/lib/rom/commands/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/command' 4 | 5 | module ROM 6 | module Commands 7 | # Create command 8 | # 9 | # This command inserts a new tuple into a relation 10 | # 11 | # @abstract 12 | class Create < Command 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /core/lib/rom/commands/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/command' 4 | 5 | module ROM 6 | module Commands 7 | # Delete command 8 | # 9 | # This command removes tuples from its target relation 10 | # 11 | # @abstract 12 | class Delete < Command 13 | restrictable true 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /core/lib/rom/commands/lazy/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Commands 5 | class Lazy 6 | # Lazy command wrapper for create commands 7 | # 8 | # @api public 9 | class Create < Lazy 10 | # Execute a command 11 | # 12 | # @see Command::Create#call 13 | # 14 | # @return [Hash,Array] 15 | # 16 | # @api public 17 | # rubocop:disable Metrics/AbcSize 18 | def call(*args) 19 | first = args.first 20 | last = args.last 21 | size = args.size 22 | 23 | if size > 1 && last.is_a?(Array) 24 | last.map.with_index do |parent, index| 25 | children = evaluator.call(first, index) 26 | command_proc[command, parent, children].call(children, parent) 27 | end.reduce(:concat) 28 | else 29 | input = evaluator.call(first) 30 | command.call(input, *args[1..size - 1]) 31 | end 32 | end 33 | # rubocop:enable Metrics/AbcSize 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/lib/rom/commands/lazy/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Commands 5 | class Lazy 6 | # Lazy command wrapper for delete commands 7 | # 8 | # @api public 9 | class Delete < Lazy 10 | # Execute a lazy delete command 11 | # 12 | # @see Commands::Delete#call 13 | # 14 | # @return [Hash, Array] 15 | # 16 | # @api public 17 | def call(*args) 18 | first = args.first 19 | last = args.last 20 | size = args.size 21 | 22 | if size > 1 && last.is_a?(Array) 23 | raise NotImplementedError 24 | else 25 | input = evaluator.call(first) 26 | 27 | if input.is_a?(Array) 28 | input.map do |item| 29 | command_proc[command, *(size > 1 ? [last, item] : [input])].call 30 | end 31 | else 32 | command_proc[command, input].call 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /core/lib/rom/commands/lazy/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Commands 5 | class Lazy 6 | # Lazy command wrapper for update commands 7 | # 8 | # @api public 9 | class Update < Lazy 10 | # Execute a lazy update command 11 | # 12 | # @see Commands::Update#call 13 | # 14 | # @return [Hash, Array] 15 | # 16 | # @api public 17 | # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity 18 | def call(*args) 19 | first = args.first 20 | last = args.last 21 | size = args.size 22 | 23 | if size > 1 && last.is_a?(Array) 24 | last.map.with_index do |parent, index| 25 | children = evaluator.call(first, index) 26 | 27 | children.map do |child| 28 | command_proc[command, parent, child].call(child, parent) 29 | end 30 | end.reduce(:concat) 31 | else 32 | input = evaluator.call(first) 33 | 34 | if input.is_a?(Array) 35 | input.map.with_index do |item, _index| 36 | command_proc[command, last, item].call(item, *args[1..size - 1]) 37 | end 38 | else 39 | command_proc[command, *(size > 1 ? [last, input] : [input])] 40 | .call(input, *args[1..size - 1]) 41 | end 42 | end 43 | end 44 | # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /core/lib/rom/commands/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/command' 4 | 5 | module ROM 6 | module Commands 7 | # Update command 8 | # 9 | # This command updates all tuples in its relation with new attributes 10 | # 11 | # @abstract 12 | class Update < Command 13 | restrictable true 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /core/lib/rom/configuration_dsl/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/inflector' 4 | 5 | module ROM 6 | module ConfigurationDSL 7 | # Setup DSL-specific command extensions 8 | # 9 | # @private 10 | class Command 11 | # Generate a command subclass 12 | # 13 | # This is used by Setup#commands DSL and its `define` block 14 | # 15 | # @api private 16 | def self.build_class(name, relation, options = EMPTY_HASH, &) 17 | type = options.fetch(:type) { name } 18 | command_type = Inflector.classify(type) 19 | adapter = options.fetch(:adapter) 20 | parent = ROM::Command.adapter_namespace(adapter).const_get(command_type) 21 | class_name = generate_class_name(adapter, command_type, relation) 22 | 23 | Dry::Core::ClassBuilder.new(name: class_name, parent: parent).call do |klass| 24 | klass.register_as(name) 25 | klass.relation(relation) 26 | klass.class_eval(&) if block_given? 27 | end 28 | end 29 | 30 | # Create a command subclass name based on adapter, type and relation 31 | # 32 | # @api private 33 | def self.generate_class_name(adapter, command_type, relation) 34 | pieces = ['ROM'] 35 | pieces << Inflector.classify(adapter) 36 | pieces << 'Commands' 37 | pieces << "#{command_type}[#{Inflector.classify(relation)}s]" 38 | pieces.join('::') 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /core/lib/rom/configuration_dsl/command_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/configuration_dsl/command' 4 | 5 | module ROM 6 | module ConfigurationDSL 7 | # Command `define` DSL used by Setup#commands 8 | # 9 | # @private 10 | class CommandDSL 11 | attr_reader :relation 12 | 13 | attr_reader :adapter 14 | 15 | attr_reader :command_classes 16 | 17 | # @api private 18 | def initialize(relation, adapter = nil, &) 19 | @relation = relation 20 | @adapter = adapter 21 | @command_classes = [] 22 | instance_exec(&) 23 | end 24 | 25 | # Define a command class 26 | # 27 | # @param [Symbol] name of the command 28 | # @param [Hash] options 29 | # @option options [Symbol] :type The type of the command 30 | # 31 | # @return [Class] generated class 32 | # 33 | # @api public 34 | def define(name, options = EMPTY_HASH, &) 35 | @command_classes << Command.build_class( 36 | name, relation, { adapter: adapter }.merge(options), & 37 | ) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /core/lib/rom/configuration_dsl/mapper_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/mapper/builder' 4 | 5 | module ROM 6 | module ConfigurationDSL 7 | # Mapper definition DSL used by Setup DSL 8 | # 9 | # @private 10 | class MapperDSL 11 | attr_reader :configuration 12 | 13 | attr_reader :mapper_classes 14 | 15 | attr_reader :defined_mappers 16 | 17 | # @api private 18 | def initialize(configuration, mapper_classes, block) 19 | @configuration = configuration 20 | @mapper_classes = mapper_classes 21 | @defined_mappers = [] 22 | 23 | instance_exec(&block) 24 | 25 | @mapper_classes = @defined_mappers 26 | end 27 | 28 | # Define a mapper class 29 | # 30 | # @param [Symbol] name of the mapper 31 | # @param [Hash] options 32 | # 33 | # @return [Class] 34 | # 35 | # @api public 36 | def define(name, options = EMPTY_HASH, &) 37 | @defined_mappers << Mapper::Builder.build_class( 38 | name, 39 | @mapper_classes + @defined_mappers, 40 | options, 41 | & 42 | ) 43 | self 44 | end 45 | 46 | # TODO 47 | # 48 | # @api public 49 | def register(relation, mappers) 50 | configuration.register_mapper(relation => mappers) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /core/lib/rom/configuration_dsl/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/inflector' 4 | 5 | module ROM 6 | module ConfigurationDSL 7 | # Setup DSL-specific relation extensions 8 | # 9 | # @private 10 | class Relation 11 | # Generate a relation subclass 12 | # 13 | # This is used by Setup#relation DSL 14 | # 15 | # @api private 16 | def self.build_class(name, options = EMPTY_HASH) 17 | class_name = "ROM::Relation[#{Inflector.camelize(name)}]" 18 | adapter = options.fetch(:adapter) 19 | 20 | ::Dry::Core::ClassBuilder.new( 21 | name: class_name, parent: ::ROM::Relation[adapter] 22 | ).call do |klass| 23 | klass.gateway(options.fetch(:gateway, :default)) 24 | klass.schema(name) { nil } 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/lib/rom/core/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Core 5 | VERSION = '5.4.2' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /core/lib/rom/global.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/plugin_registry' 4 | require 'rom/global/plugin_dsl' 5 | 6 | module ROM 7 | # Globally accessible public interface exposed via ROM module 8 | # 9 | # @api public 10 | module Global 11 | # Set base global registries in ROM constant 12 | # 13 | # @api private 14 | def self.extended(rom) 15 | super 16 | 17 | rom.instance_variable_set('@adapters', {}) 18 | rom.instance_variable_set('@plugin_registry', PluginRegistry.new) 19 | end 20 | 21 | # An internal adapter identifier => adapter module map used by setup 22 | # 23 | # @return [HashModule>] 24 | # 25 | # @api private 26 | attr_reader :adapters 27 | 28 | # An internal identifier => plugin map used by the setup 29 | # 30 | # @return [Hash] 31 | # 32 | # @api private 33 | attr_reader :plugin_registry 34 | 35 | # Global plugin setup DSL 36 | # 37 | # @example 38 | # ROM.plugins do 39 | # register :publisher, Plugin::Publisher, type: :command 40 | # end 41 | # 42 | # @api public 43 | def plugins(...) 44 | PluginDSL.new(plugin_registry, ...) 45 | end 46 | 47 | # Register adapter namespace under a specified identifier 48 | # 49 | # @param [Symbol] identifier 50 | # @param [Class,Module] adapter 51 | # 52 | # @return [self] 53 | # 54 | # @api private 55 | def register_adapter(identifier, adapter) 56 | adapters[identifier] = adapter 57 | self 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /core/lib/rom/global/plugin_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Global 5 | # plugin registration DSL 6 | # 7 | # @private 8 | class PluginDSL 9 | # Default options passed to plugin registration 10 | # 11 | # @return [Hash] 12 | # 13 | # @api private 14 | attr_reader :defaults 15 | 16 | # Plugin registry 17 | # 18 | # @return [PluginRegistry] 19 | # 20 | # @api private 21 | attr_reader :registry 22 | 23 | # @api private 24 | def initialize(registry, defaults = EMPTY_HASH, &) 25 | @registry = registry 26 | @defaults = defaults 27 | instance_exec(&) 28 | end 29 | 30 | # Register a plugin 31 | # 32 | # @param [Symbol] name of the plugin 33 | # @param [Module] mod to include 34 | # @param [Hash] options 35 | # 36 | # @api public 37 | def register(name, mod, options = EMPTY_HASH) 38 | registry.register(name, mod, defaults.merge(options)) 39 | end 40 | 41 | # Register plugins for a specific adapter 42 | # 43 | # @param [Symbol] type The adapter identifier 44 | # 45 | # @api public 46 | def adapter(type, &) 47 | self.class.new(registry, adapter: type, &) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /core/lib/rom/lint/enumerable_dataset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/lint/linter' 4 | 5 | module ROM 6 | module Lint 7 | # Ensures that a [ROM::EnumerableDataset] extension correctly yields 8 | # arrays and tuples 9 | # 10 | # @api public 11 | class EnumerableDataset < ROM::Lint::Linter 12 | # The linted subject 13 | # 14 | # @api public 15 | attr_reader :dataset 16 | 17 | # The expected data 18 | # 19 | # @api public 20 | attr_reader :data 21 | 22 | # Create a linter for EnumerableDataset 23 | # 24 | # @param [EnumerableDataset] dataset the linted subject 25 | # @param [Object] data the expected data 26 | # 27 | # @api public 28 | def initialize(dataset, data) 29 | super() 30 | @dataset = dataset 31 | @data = data 32 | end 33 | 34 | # Lint: Ensure that +dataset+ yield tuples via +each+ 35 | # 36 | # @api public 37 | def lint_each 38 | result = [] 39 | dataset.each do |tuple| # rubocop:disable Style/MapIntoArray 40 | result << tuple 41 | end 42 | return if result == data 43 | 44 | complain "#{dataset.class}#each must yield tuples" 45 | end 46 | 47 | # Lint: Ensure that +dataset+'s array equals to expected +data+ 48 | # 49 | # @api public 50 | def lint_to_a 51 | return if dataset.to_a == data 52 | 53 | complain "#{dataset.class}#to_a must cast dataset to an array" 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /core/lib/rom/lint/spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/lint/gateway' 4 | require 'rom/lint/enumerable_dataset' 5 | 6 | RSpec.shared_examples 'a rom gateway' do 7 | ROM::Lint::Gateway.each_lint do |name, linter| 8 | it name do 9 | result = linter.new(identifier, gateway, uri).lint(name) 10 | expect(result).to be_truthy 11 | end 12 | end 13 | end 14 | 15 | RSpec.shared_examples 'a rom enumerable dataset' do 16 | ROM::Lint::EnumerableDataset.each_lint do |name, linter| 17 | it name do 18 | result = linter.new(dataset, data).lint(name) 19 | expect(result).to be_truthy 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /core/lib/rom/mapper/builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Mapper 5 | # Setup DSL-specific mapper extensions 6 | # 7 | # @private 8 | class Builder 9 | # Generate a mapper subclass 10 | # 11 | # This is used by Setup#mappers DSL 12 | # 13 | # @api private 14 | def self.build_class(name, mapper_registry, options = EMPTY_HASH, &) 15 | class_name = "ROM::Mapper[#{name}]" 16 | 17 | parent = options[:parent] 18 | inherit_header = options.fetch(:inherit_header) { ROM::Mapper.inherit_header } 19 | 20 | parent_class = 21 | if parent 22 | mapper_registry.detect { |klass| klass.relation == parent } 23 | else 24 | ROM::Mapper 25 | end 26 | 27 | Dry::Core::ClassBuilder.new(name: class_name, parent: parent_class).call do |klass| 28 | klass.register_as(name) 29 | klass.relation(name) 30 | klass.inherit_header(inherit_header) 31 | 32 | klass.class_eval(&) if block_given? 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/lib/rom/mapper_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/struct' 4 | require 'rom/registry' 5 | require 'rom/mapper_compiler' 6 | 7 | module ROM 8 | # @private 9 | class MapperRegistry < Registry 10 | # @api private 11 | def self.element_not_found_error = MapperMissingError 12 | 13 | # @!attribute [r] compiler 14 | # @return [MapperCompiler] A mapper compiler instance 15 | option :compiler, default: lambda { 16 | MapperCompiler.new(EMPTY_HASH, cache: cache) 17 | } 18 | 19 | # @see Registry 20 | # @api public 21 | def [](*args) 22 | if args[0].is_a?(::Symbol) 23 | super 24 | else 25 | cache.fetch_or_store(args.hash) { compiler.(*args) } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/lib/rom/memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory/gateway' 4 | require 'rom/memory/relation' 5 | require 'rom/memory/mapper_compiler' 6 | 7 | ROM.register_adapter(:memory, ROM::Memory) 8 | -------------------------------------------------------------------------------- /core/lib/rom/memory/associations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory/associations/many_to_many' 4 | require 'rom/memory/associations/many_to_one' 5 | require 'rom/memory/associations/one_to_many' 6 | require 'rom/memory/associations/one_to_one' 7 | -------------------------------------------------------------------------------- /core/lib/rom/memory/associations/many_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/many_to_many' 4 | 5 | module ROM 6 | module Memory 7 | module Associations 8 | class ManyToMany < ROM::Associations::ManyToMany 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/lib/rom/memory/associations/many_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/many_to_one' 4 | 5 | module ROM 6 | module Memory 7 | module Associations 8 | class ManyToOne < ROM::Associations::ManyToOne 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/lib/rom/memory/associations/one_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/associations/one_to_many' 4 | 5 | module ROM 6 | module Memory 7 | module Associations 8 | class OneToMany < ROM::Associations::OneToMany 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/lib/rom/memory/associations/one_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory/associations/one_to_many' 4 | 5 | module ROM 6 | module Memory 7 | module Associations 8 | class OneToOne < OneToMany 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/lib/rom/memory/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/commands' 4 | 5 | module ROM 6 | module Memory 7 | # Memory adapter commands namespace 8 | # 9 | # @api public 10 | module Commands 11 | # In-memory create command 12 | # 13 | # @api public 14 | class Create < ROM::Commands::Create 15 | adapter :memory 16 | use :schema 17 | 18 | # @see ROM::Commands::Create#execute 19 | def execute(tuples) 20 | [tuples].flatten.map { |tuple| 21 | attributes = input[tuple] 22 | relation.insert(attributes.to_h) 23 | attributes 24 | }.to_a 25 | end 26 | end 27 | 28 | # In-memory update command 29 | # 30 | # @api public 31 | class Update < ROM::Commands::Update 32 | adapter :memory 33 | use :schema 34 | 35 | # @see ROM::Commands::Update#execute 36 | def execute(params) 37 | attributes = input[params] 38 | relation.map { |tuple| tuple.update(attributes.to_h) } 39 | end 40 | end 41 | 42 | # In-memory delete command 43 | # 44 | # @api public 45 | class Delete < ROM::Commands::Delete 46 | adapter :memory 47 | 48 | # @see ROM::Commands::Delete#execute 49 | def execute 50 | relation.to_a.map do |tuple| 51 | source.delete(tuple) 52 | tuple 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /core/lib/rom/memory/mapper_compiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/mapper_compiler' 4 | 5 | module ROM 6 | module Memory 7 | class MapperCompiler < ROM::MapperCompiler 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/lib/rom/memory/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | require 'rom/memory/associations' 5 | 6 | module ROM 7 | module Memory 8 | # Specialized schema for memory adapter 9 | # 10 | # @api public 11 | class Schema < ROM::Schema 12 | # @see Schema#call 13 | # @api public 14 | def call(relation) 15 | relation.new(relation.dataset.project(*map(&:name)), schema: self) 16 | end 17 | 18 | # Internal hook used during setup process 19 | # 20 | # @see Schema#finalize_associations! 21 | # 22 | # @api private 23 | def finalize_associations!(relations:) 24 | super do 25 | associations.map do |definition| 26 | Memory::Associations.const_get(definition.type).new(definition, relations) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /core/lib/rom/memory/storage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'concurrent/hash' 4 | require 'concurrent/array' 5 | 6 | require 'rom/memory/dataset' 7 | 8 | module ROM 9 | module Memory 10 | # In-memory thread-safe data storage 11 | # 12 | # @private 13 | class Storage 14 | # Dataset registry 15 | # 16 | # @return [ThreadSafe::Hash] 17 | # 18 | # @api private 19 | attr_reader :data 20 | 21 | # @api private 22 | def initialize 23 | @data = Concurrent::Hash.new 24 | end 25 | 26 | # @return [Dataset] 27 | # 28 | # @api private 29 | def [](name) 30 | data[name] 31 | end 32 | 33 | # Register a new dataset 34 | # 35 | # @return [Dataset] 36 | # 37 | # @api private 38 | def create_dataset(name) 39 | data[name] = Dataset.new(Concurrent::Array.new) 40 | end 41 | 42 | # Check if there's dataset under specified key 43 | # 44 | # @return [Boolean] 45 | # 46 | # @api private 47 | def key?(name) 48 | data.key?(name) 49 | end 50 | 51 | # Return registered datasets count 52 | # 53 | # @return [Integer] 54 | # 55 | # @api private 56 | def size 57 | data.size 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /core/lib/rom/memory/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | 5 | module ROM 6 | module Memory 7 | module Types 8 | include ROM::Types 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /core/lib/rom/open_struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | # ROM's open structs are used for relations with empty schemas. 5 | # Such relations may exist in cases like using raw SQL strings 6 | # where schema was not explicitly defined using `view` DSL. 7 | # 8 | # @api public 9 | class OpenStruct 10 | IVAR = -> v { :"@#{v}" } 11 | 12 | # @api private 13 | def initialize(attributes) 14 | attributes.each do |key, value| 15 | instance_variable_set(IVAR[key], value) 16 | end 17 | end 18 | 19 | # @api private 20 | def respond_to_missing?(meth, include_private = false) 21 | instance_variable_defined?(IVAR[meth]) || super 22 | end 23 | 24 | private 25 | 26 | # @api private 27 | def method_missing(meth, *, &) 28 | ivar = IVAR[meth] 29 | 30 | if instance_variable_defined?(ivar) 31 | instance_variable_get(ivar) 32 | else 33 | super 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/lib/rom/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/initializer' 4 | require 'rom/constants' 5 | require 'rom/support/configurable' 6 | 7 | module ROM 8 | # Plugin is a simple object used to store plugin configurations 9 | # 10 | # @private 11 | class Plugin 12 | extend Initializer 13 | include Configurable 14 | 15 | # @!attribute [r] name 16 | # @return [Symbol] plugin name 17 | # @api private 18 | param :name 19 | 20 | # @!attribute [r] mod 21 | # @return [Module] a module representing the plugin 22 | # @api private 23 | param :mod 24 | 25 | # @!attribute [r] type 26 | # @return [Symbol] plugin type 27 | # @api private 28 | option :type 29 | 30 | # Apply this plugin to the target 31 | # 32 | # @param [Class,Object] target 33 | # 34 | # @api private 35 | def apply_to(target, **options) 36 | if mod.respond_to?(:apply) 37 | mod.apply(target, **options) 38 | elsif mod.respond_to?(:new) 39 | target.include(mod.new(**options)) 40 | elsif target.is_a?(::Module) 41 | target.include(mod) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /core/lib/rom/plugins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/plugin' 4 | 5 | module ROM 6 | # Registry of all known plugin types (command, relation, mapper, etc) 7 | # 8 | # @api private 9 | module Plugins 10 | extend ::Dry::Core::Container::Mixin 11 | 12 | class << self 13 | # @api private 14 | def register(entity_type, plugin_type: Plugin, adapter: true) = super 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/lib/rom/plugins/command/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Plugins 5 | module Command 6 | # Command plugin which sets input processing function via relation schema 7 | # 8 | # @api private 9 | module Schema 10 | def self.included(klass) 11 | super 12 | klass.extend(ClassInterface) 13 | end 14 | 15 | # @api private 16 | module ClassInterface 17 | # Build a command and set it input to relation's input_schema 18 | # 19 | # @see Command.build 20 | # 21 | # @return [Command] 22 | # 23 | # @api public 24 | def build(relation, **options) 25 | if relation.schema? && !options.key?(:input) 26 | relation_input = relation.input_schema 27 | command_input = input 28 | 29 | composed_input = 30 | if command_input.equal?(ROM::Command.input) 31 | relation_input 32 | else 33 | -> tuple { relation_input[command_input[tuple]] } 34 | end 35 | 36 | super(relation, **options, input: composed_input) 37 | else 38 | super 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /core/lib/rom/plugins/relation/registry_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/constants' 4 | 5 | module ROM 6 | module Plugins 7 | module Relation 8 | # Allows relations to access all other relations through registry 9 | # 10 | # For now this plugin is always enabled 11 | # 12 | # @api public 13 | class RegistryReader < ::Module 14 | EMPTY_REGISTRY = RelationRegistry.build(EMPTY_HASH).freeze 15 | 16 | # @api private 17 | def initialize(readers:) 18 | super() 19 | include readers 20 | end 21 | 22 | # @api private 23 | def included(klass) 24 | super 25 | return if klass.instance_methods.include?(:__registry__) 26 | 27 | klass.option :__registry__, default: -> { EMPTY_REGISTRY } 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /core/lib/rom/processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/mapper' 4 | 5 | module ROM 6 | # Abstract processor class 7 | # 8 | # Every ROM processor should inherit from this class 9 | # 10 | # @api public 11 | class Processor 12 | # Hook used to auto-register a processor class 13 | # 14 | # @api private 15 | def self.inherited(processor) 16 | super 17 | Mapper.register_processor(processor) 18 | end 19 | 20 | # Required interface to be implemented by descendants 21 | # 22 | # @return [Processor] 23 | # 24 | # @abstract 25 | # 26 | # @api private 27 | def self.build 28 | raise NotImplementedError, '+build+ must be implemented' 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /core/lib/rom/relation/composite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/loaded' 4 | require 'rom/relation/materializable' 5 | require 'rom/pipeline' 6 | 7 | module ROM 8 | class Relation 9 | # Left-to-right relation composition used for data-pipelining 10 | # 11 | # @api public 12 | class Composite < Pipeline::Composite 13 | include Materializable 14 | 15 | # Call the pipeline by passing results from left to right 16 | # 17 | # Optional args are passed to the left object 18 | # 19 | # @return [Loaded] 20 | # 21 | # @api public 22 | def call(*args) 23 | relation = left.call(*args) 24 | response = right.call(relation) 25 | 26 | if response.is_a?(Loaded) 27 | response 28 | else 29 | relation.new(response) 30 | end 31 | end 32 | alias_method :[], :call 33 | 34 | # @see Relation#map_to 35 | # 36 | # @api public 37 | def map_to(klass) = self >> left.map_to(klass).mapper 38 | 39 | private 40 | 41 | # @api private 42 | # 43 | # @see Pipeline::Proxy#decorate? 44 | # 45 | # @api private 46 | def decorate?(response) = super || response.is_a?(Graph) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /core/lib/rom/relation/materializable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Relation 5 | # Interface for objects that can be materialized into a loaded relation 6 | # 7 | # @api public 8 | module Materializable 9 | # Coerce the relation to an array 10 | # 11 | # @return [Array] 12 | # 13 | # @api public 14 | def to_a = call.to_a 15 | alias_method :to_ary, :to_a 16 | 17 | # Yield relation tuples 18 | # 19 | # @yield [Hash,Object] 20 | # 21 | # @api public 22 | def each(&) 23 | return to_enum unless block_given? 24 | 25 | to_a.each(&) 26 | end 27 | 28 | # Delegate to loaded relation and return one object 29 | # 30 | # @return [Object] 31 | # 32 | # @see Loaded#one 33 | # 34 | # @api public 35 | def one = call.one 36 | 37 | # Delegate to loaded relation and return one object 38 | # 39 | # @return [Object] 40 | # 41 | # @see Loaded#one 42 | # 43 | # @api public 44 | def one! = call.one! 45 | 46 | # Return first tuple from a relation coerced to an array 47 | # 48 | # @return [Object] 49 | # 50 | # @api public 51 | def first = to_a.first 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /core/lib/rom/relation/wrap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/graph' 4 | require 'rom/relation/combined' 5 | 6 | module ROM 7 | class Relation 8 | # Relation wrapping other relations 9 | # 10 | # @api public 11 | class Wrap < Graph 12 | # Wrap more relations 13 | # 14 | # @see Relation#wrap 15 | # 16 | # @return [Wrap] 17 | # 18 | # @api public 19 | def wrap(*args) 20 | self.class.new(root, nodes + root.wrap(*args).nodes) 21 | end 22 | 23 | # Materialize a wrap 24 | # 25 | # @see Relation#call 26 | # 27 | # @return [Loaded] 28 | # 29 | # @api public 30 | def call(*args) 31 | if auto_map? 32 | Loaded.new(self, mapper.(relation.with(auto_map: false, auto_struct: false))) 33 | else 34 | Loaded.new(self, relation.(*args)) 35 | end 36 | end 37 | 38 | # Return an adapter-specific relation representing a wrap 39 | # 40 | # @abstract 41 | # 42 | # @api private 43 | def relation = raise ::NotImplementedError 44 | 45 | # Return if this is a wrap relation 46 | # 47 | # @return [true] 48 | # 49 | # @api private 50 | def wrap? = true 51 | 52 | private 53 | 54 | # @api private 55 | def decorate?(other) = super || other.is_a?(Combined) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /core/lib/rom/relation_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/registry' 4 | 5 | module ROM 6 | # @api private 7 | class RelationRegistry < Registry 8 | # @api private 9 | def initialize(elements = {}, **) 10 | super 11 | yield(self, elements) if block_given? 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/lib/rom/schema_plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/plugin' 4 | 5 | module ROM 6 | # @api private 7 | class SchemaPlugin < Plugin 8 | # Extends a DSL instance with a module provided by the plugin 9 | # 10 | # @param [ROM::Schema::DSL] dsl 11 | # 12 | # @api private 13 | def extend_dsl(dsl) 14 | dsl.extend(mod.const_get(:DSL)) if mod.const_defined?(:DSL) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/lib/rom/setup/auto_registration_strategies/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | require 'rom/initializer' 5 | 6 | module ROM 7 | module AutoRegistrationStrategies 8 | # Base class for registration strategies 9 | # 10 | # @api private 11 | class Base 12 | extend Initializer 13 | 14 | PathnameType = Types.Instance(Pathname) 15 | 16 | EXTENSION_REGEX = /\.rb\z/ 17 | 18 | # @!attribute [r] file 19 | # @return [String] Name of a component file 20 | option :file, type: Types::Strict::String 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /core/lib/rom/setup/auto_registration_strategies/no_namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | require 'rom/support/inflector' 6 | require 'rom/types' 7 | require 'rom/setup/auto_registration_strategies/base' 8 | 9 | module ROM 10 | module AutoRegistrationStrategies 11 | # NoNamespace strategy assumes components are not defined within a namespace 12 | # 13 | # @api private 14 | class NoNamespace < Base 15 | # @!attribute [r] directory 16 | # @return [Pathname] The path to dir with components 17 | option :directory, type: PathnameType 18 | 19 | # @!attribute [r] entity 20 | # @return [Symbol] Component identifier 21 | option :entity, type: Types::Strict::Symbol 22 | 23 | # Load components 24 | # 25 | # @api private 26 | def call 27 | Inflector.camelize( 28 | file.sub(%r{^#{directory}/#{entity}/}, '').sub(EXTENSION_REGEX, '') 29 | ) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /core/lib/rom/setup/auto_registration_strategies/with_namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | require 'rom/support/inflector' 6 | require 'rom/setup/auto_registration_strategies/base' 7 | 8 | module ROM 9 | module AutoRegistrationStrategies 10 | # WithNamespace strategy assumes components are defined within a namespace 11 | # that matches top-level directory name. 12 | # 13 | # @api private 14 | class WithNamespace < Base 15 | # @!attribute [r] directory 16 | # @return [Pathname] The path to dir with components 17 | option :directory, type: PathnameType 18 | 19 | # Load components 20 | # 21 | # @api private 22 | def call 23 | Inflector.camelize( 24 | file.sub(%r{^#{directory.dirname}/}, '').sub(EXTENSION_REGEX, '') 25 | ) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/lib/rom/support/inflector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | Inflector = Dry::Inflector.new do |i| 5 | i.plural(/people\z/i, 'people') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /core/lib/rom/support/memoizable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | # @api private 5 | module Memoizable 6 | MEMOIZED_HASH = {}.freeze 7 | 8 | module ClassInterface 9 | # @api private 10 | def memoize(*names) 11 | prepend(Memoizer.new(self, names)) 12 | end 13 | 14 | def new(*, **, &) 15 | obj = super 16 | obj.instance_variable_set(:@__memoized__, MEMOIZED_HASH.dup) 17 | obj 18 | end 19 | end 20 | 21 | def self.included(klass) 22 | super 23 | klass.extend(ClassInterface) 24 | end 25 | 26 | attr_reader :__memoized__ 27 | 28 | # @api private 29 | class Memoizer < ::Module 30 | attr_reader :klass 31 | 32 | attr_reader :names 33 | 34 | # @api private 35 | def initialize(klass, names) 36 | super() 37 | @names = names 38 | @klass = klass 39 | define_memoizable_names! 40 | end 41 | 42 | private 43 | 44 | # @api private 45 | def define_memoizable_names! 46 | names.each do |name| 47 | meth = klass.instance_method(name) 48 | 49 | if meth.parameters.empty? 50 | define_method(name) do 51 | __memoized__[name] ||= super() 52 | end 53 | else 54 | define_method(name) do |*args| 55 | __memoized__[:"#{name}_#{args.hash}"] ||= super(*args) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /core/lib/rom/transaction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | # @api private 5 | class Transaction 6 | # @api private 7 | Rollback = ::Class.new(::StandardError) 8 | 9 | # @api private 10 | def run(_opts = EMPTY_HASH) 11 | yield(self) 12 | rescue Rollback 13 | # noop 14 | end 15 | 16 | # Unconditionally roll back the current transaction 17 | # 18 | # @api public 19 | def rollback! = raise(Rollback) 20 | 21 | # @api private 22 | NoOp = Transaction.new.freeze 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /core/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rom-rb/rom/d2de00f6249d17aea7965573972633677018f4cf/core/log/.gitkeep -------------------------------------------------------------------------------- /core/rakelib/benchmark.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc 'Run benchmarks (tweak count via COUNT envvar)' 4 | task :benchmark do 5 | FileList['benchmarks/**/*_bench.rb'].each do |bench| 6 | sh "ruby #{bench}" 7 | end 8 | end 9 | 10 | namespace :benchmark do 11 | desc 'Verify benchmarks' 12 | task :verify do 13 | ENV['VERIFY'] = 'true' 14 | ENV['COUNT'] = '1' 15 | Rake::Task[:benchmark].invoke 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/rakelib/rubocop.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'rubocop/rake_task' 5 | 6 | Rake::Task[:default].enhance [:rubocop] 7 | 8 | RuboCop::RakeTask.new do |task| 9 | task.options << '--display-cop-names' 10 | end 11 | 12 | namespace :rubocop do 13 | desc 'Generate a configuration file acting as a TODO list.' 14 | task :auto_gen_config do 15 | exec 'bundle exec rubocop --auto-gen-config' 16 | end 17 | end 18 | rescue LoadError 19 | # ignore 20 | end 21 | -------------------------------------------------------------------------------- /core/rom-core.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/rom/core/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'rom-core' 7 | gem.summary = 'Ruby Object Mapper' 8 | gem.description = 'Persistence and mapping toolkit for Ruby' 9 | gem.author = 'Piotr Solnica' 10 | gem.email = 'piotr.solnica+oss@gmail.com' 11 | gem.homepage = 'http://rom-rb.org' 12 | gem.require_paths = ['lib'] 13 | gem.version = ROM::Core::VERSION.dup 14 | gem.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'lib/**/*'] 15 | gem.license = 'MIT' 16 | gem.metadata = { 17 | 'source_code_uri' => 'https://github.com/rom-rb/rom/tree/master/core', 18 | 'documentation_uri' => 'https://api.rom-rb.org/rom/', 19 | 'mailing_list_uri' => 'https://discourse.rom-rb.org/', 20 | 'bug_tracker_uri' => 'https://github.com/rom-rb/rom/issues', 21 | 'rubygems_mfa_required' => 'true' 22 | } 23 | 24 | gem.required_ruby_version = '>= 3.1.0' 25 | 26 | gem.add_dependency 'concurrent-ruby', '~> 1.1' 27 | gem.add_dependency 'dry-configurable', '~> 1.0' 28 | gem.add_dependency 'dry-core', '~> 1.0' 29 | gem.add_dependency 'dry-inflector', '~> 1.0' 30 | gem.add_dependency 'dry-initializer', '~> 3.2' 31 | gem.add_dependency 'dry-struct', '~> 1.0' 32 | gem.add_dependency 'dry-types', '~> 1.6' 33 | gem.add_dependency 'transproc', '~> 1.1' 34 | end 35 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/commands/create_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUser 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/mappers/user_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserList 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/my_commands/create_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUser 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/my_mappers/user_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserList 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/my_relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Users 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/app/relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Users 4 | end 5 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom/commands/create_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | class CreateUser 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom/mappers/user_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | class UserList 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom/relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | class Users 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom_namespace/commands/create_customer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | module Commands 6 | class CreateCustomer 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom_namespace/mappers/customer_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | module Mappers 6 | class CustomerList 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/fixtures/custom_namespace/relations/customers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module Namespace 5 | module Relations 6 | class Customers 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/commands/create_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module Commands 5 | class CreateUser 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/mappers/user_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module Mappers 5 | class UserList 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/my_commands/create_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module MyCommands 5 | class CreateUser 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/my_mappers/user_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module MyMappers 5 | class UserList 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/my_relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module MyRelations 5 | class Users 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/lib/persistence/relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module Relations 5 | class Users 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/fixtures/wrong/commands/create_customer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module NewNamespace 5 | module Foo 6 | class CreateCustomer 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/fixtures/wrong/mappers/customer_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module NewNamespace 5 | module Foo 6 | class CustomerList 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/fixtures/wrong/relations/customers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module My 4 | module NewNamespace 5 | module Foo 6 | class Customers 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /core/spec/integration/command_registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'ROM::CommandRegistry' do 6 | include_context 'container' 7 | 8 | let(:users) { container.commands[:users] } 9 | 10 | before do 11 | configuration.relation(:users) 12 | 13 | configuration.register_command(Class.new(ROM::Commands::Create[:memory]) do 14 | register_as :create 15 | relation :users 16 | end) 17 | end 18 | 19 | describe '#[]' do 20 | it 'fetches a command from the registry' do 21 | expect(users[:create]).to be_a(ROM::Commands::Create[:memory]) 22 | end 23 | 24 | it 'throws an error when the command is not found' do 25 | expect { users[:not_found] }.to raise_error( 26 | ROM::CommandNotFoundError, 27 | 'There is no :not_found command for :users relation' 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /core/spec/integration/gateways/extending_relations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/memory' 5 | 6 | RSpec.describe 'Gateways / Extending Relations' do 7 | include_context 'container' 8 | include_context 'users and tasks' 9 | 10 | before do 11 | module ROM 12 | module Memory 13 | class Relation < ROM::Relation 14 | schema(:users) {} 15 | 16 | def self.freaking_awesome? 17 | true 18 | end 19 | 20 | def freaking_cool? 21 | true 22 | end 23 | end 24 | end 25 | end 26 | end 27 | 28 | after do 29 | ROM::Memory::Relation.class_eval do 30 | undef_method :freaking_cool? 31 | class << self 32 | undef_method :freaking_awesome? 33 | end 34 | end 35 | end 36 | 37 | shared_examples_for 'extended relation' do 38 | it 'can extend relation class' do 39 | expect(container.relations.users.class).to be_freaking_awesome 40 | end 41 | 42 | it 'can extend relation instance' do 43 | expect(container.relations.users).to be_freaking_cool 44 | end 45 | end 46 | 47 | context 'using DSL' do 48 | it_behaves_like 'extended relation' do 49 | before do 50 | configuration.relation(:users) 51 | end 52 | end 53 | end 54 | 55 | context 'using class definition' do 56 | it_behaves_like 'extended relation' do 57 | before do 58 | configuration.register_relation(Class.new(ROM::Relation[:memory]) { schema(:users) {} }) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /core/spec/integration/gateways/setting_logger_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/memory' 5 | 6 | require 'logger' 7 | 8 | RSpec.describe 'Gateways / Setting logger' do 9 | let(:logger_class) do 10 | Class.new do 11 | attr_reader :messages 12 | 13 | def initialize 14 | @messages = [] 15 | end 16 | 17 | def info(msg) 18 | @messages << msg 19 | end 20 | end 21 | end 22 | 23 | let(:logger) do 24 | logger_class.new 25 | end 26 | 27 | it 'sets up a logger for a given gateway' do 28 | container = ROM.container(:memory) do |config| 29 | config.gateways[:default].use_logger(logger) 30 | end 31 | 32 | container.gateways[:default].logger.info('test') 33 | 34 | expect(logger.messages).to eql(['test']) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/deep_embedded_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mappers / deeply embedded tuples' do 6 | include_context 'container' 7 | 8 | it 'allows mapping embedded tuples' do 9 | configuration.relation(:users) 10 | 11 | configuration.mappers do 12 | define(:users) do 13 | model name: 'Test::User' 14 | 15 | attribute :name, from: 'name' 16 | 17 | embedded :tasks, from: 'tasks' do 18 | attribute :title, from: 'title' 19 | 20 | embedded :priority, from: 'priority', type: :hash do 21 | attribute :value, from: 'value' 22 | attribute :desc, from: 'desc' 23 | end 24 | end 25 | end 26 | end 27 | 28 | container.relations.users << { 29 | 'name' => 'Jane', 30 | 'tasks' => [ 31 | { 'title' => 'Task One', 'priority' => { 'value' => 1, 'desc' => 'high' } }, 32 | { 'title' => 'Task Two', 'priority' => { 'value' => 3, 'desc' => 'low' } } 33 | ] 34 | } 35 | 36 | jane = container.relations[:users].map_with(:users).first 37 | 38 | expect(jane.name).to eql('Jane') 39 | 40 | expect(jane.tasks).to eql([ 41 | { title: 'Task One', priority: { value: 1, desc: 'high' } }, 42 | { title: 'Task Two', priority: { value: 3, desc: 'low' } } 43 | ]) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/exclude_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mapper definition DSL' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.relation(:users) 10 | 11 | users = configuration.default.dataset(:users) 12 | 13 | users.insert(name: 'Joe', email: 'joe@doe.com') 14 | users.insert(name: 'Jane', email: 'jane@doe.com') 15 | end 16 | 17 | describe 'exclude' do 18 | let(:mapped_users) { container.relations[:users].map_with(:users).to_a } 19 | 20 | it 'removes the attribute' do 21 | configuration.mappers do 22 | define(:users) { exclude :email } 23 | end 24 | 25 | expect(mapped_users).to eql [{ name: 'Joe' }, { name: 'Jane' }] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/overwrite_attributes_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mappers / Attributes value' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.relation(:users) 10 | end 11 | 12 | it 'allows to manipulate attribute value' do 13 | class Test::UserMapper < ROM::Mapper 14 | relation :users 15 | 16 | attribute :id 17 | attribute :name, from: :first_name do 18 | 'John' 19 | end 20 | attribute :age do 21 | 9 + 9 22 | end 23 | attribute :weight do |t| 24 | t + 15 25 | end 26 | end 27 | 28 | configuration.register_mapper(Test::UserMapper) 29 | 30 | container.relations.users << { 31 | id: 123, 32 | first_name: 'Jane', 33 | weight: 75 34 | } 35 | 36 | jane = container.relations[:users].map_with(:users).first 37 | 38 | expect(jane).to eql(id: 123, name: 'John', weight: 90, age: 18) 39 | end 40 | 41 | it 'raise ArgumentError if type and block used at the same time' do 42 | expect { 43 | class Test::UserMapper < ROM::Mapper 44 | relation :users 45 | 46 | attribute :name, type: :string do 47 | 'John' 48 | end 49 | end 50 | }.to raise_error(ArgumentError, "can't specify type and block at the same time") 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/prefix_separator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mapper definition DSL' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.relation(:users) 10 | 11 | users = configuration.default.dataset(:users) 12 | users.insert( 13 | user_id: 1, 14 | user_name: 'Joe', 15 | user_email: 'joe@doe.com', 16 | 'user.skype' => 'joe', 17 | :'user.phone' => '1234567890' 18 | ) 19 | end 20 | 21 | describe 'prefix' do 22 | subject(:mapped_users) { container.relations[:users].map_with(:users).to_a } 23 | 24 | it 'applies new separator to the attributes following it' do 25 | configuration.mappers do 26 | define(:users) do 27 | prefix :user 28 | attribute :id 29 | attribute :name 30 | wrap :contacts do 31 | attribute :email 32 | 33 | prefix_separator '.' 34 | attribute 'skype' 35 | attribute :phone 36 | end 37 | end 38 | end 39 | 40 | expect(mapped_users).to eql [ 41 | { 42 | id: 1, 43 | name: 'Joe', 44 | contacts: { 45 | email: 'joe@doe.com', 46 | 'skype' => 'joe', 47 | phone: '1234567890' 48 | } 49 | } 50 | ] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/prefix_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mapper definition DSL' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.relation(:users) 10 | 11 | users = configuration.default.dataset(:users) 12 | users.insert( 13 | user_id: 1, 14 | user_name: 'Joe', 15 | user_email: 'joe@doe.com', 16 | contact_skype: 'joe', 17 | contact_phone: '1234567890' 18 | ) 19 | end 20 | 21 | describe 'prefix' do 22 | subject(:mapped_users) { container.relations[:users].map_with(:users).to_a } 23 | 24 | it 'applies new prefix to the attributes following it' do 25 | configuration.mappers do 26 | define(:users) do 27 | prefix :user 28 | attribute :id 29 | attribute :name 30 | wrap :contacts do 31 | attribute :email 32 | 33 | prefix :contact 34 | attribute :skype 35 | attribute :phone 36 | end 37 | end 38 | end 39 | 40 | expect(mapped_users).to eql [ 41 | { 42 | id: 1, 43 | name: 'Joe', 44 | contacts: { email: 'joe@doe.com', skype: 'joe', phone: '1234567890' } 45 | } 46 | ] 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/prefixing_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mappers / Prefixing attributes' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.relation(:users) 10 | end 11 | 12 | it 'automatically maps all attributes using the provided prefix' do 13 | class Test::UserMapper < ROM::Mapper 14 | relation :users 15 | prefix :user 16 | 17 | model name: 'Test::User' 18 | 19 | attribute :id 20 | attribute :name 21 | attribute :email 22 | end 23 | 24 | configuration.register_mapper(Test::UserMapper) 25 | 26 | container.relations.users << { 27 | user_id: 123, 28 | user_name: 'Jane', 29 | user_email: 'jane@doe.org' 30 | } 31 | 32 | Test::User.send(:include, Dry::Equalizer(:id, :name, :email)) 33 | 34 | jane = container.relations[:users].map_with(:users).first 35 | 36 | expect(jane).to eql(Test::User.new(id: 123, name: 'Jane', email: 'jane@doe.org')) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/registering_custom_mappers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Registering Custom Mappers' do 6 | include_context 'container' 7 | include_context 'users and tasks' 8 | 9 | it 'allows registering arbitrary objects as mappers' do 10 | model = Struct.new(:name, :email) 11 | 12 | mapper = lambda { |users| 13 | users.map { |tuple| model.new(*tuple.values_at(:name, :email)) } 14 | } 15 | 16 | configuration.relation(:users) do 17 | def by_name(name) 18 | restrict(name: name) 19 | end 20 | end 21 | 22 | configuration.mappers do 23 | register(:users, entity: mapper) 24 | end 25 | 26 | users = container.relations[:users].by_name('Jane').map_with(:entity) 27 | 28 | expect(users).to match_array([model.new('Jane', 'jane@doe.org')]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/reusing_mappers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Reusing mappers' do 6 | it 'allows using another mapper in mapping definitions' do 7 | class Test::TaskMapper < ROM::Mapper 8 | attribute :title 9 | end 10 | 11 | class Test::UserMapper < ROM::Mapper 12 | attribute :name 13 | group :tasks, mapper: Test::TaskMapper 14 | end 15 | 16 | mapper = Test::UserMapper.build 17 | relation = [{ name: 'Jane', title: 'One' }, { name: 'Jane', title: 'Two' }] 18 | result = mapper.call(relation) 19 | 20 | expect(result).to eql([ 21 | { name: 'Jane', tasks: [{ title: 'One' }, { title: 'Two' }] } 22 | ]) 23 | end 24 | 25 | it 'allows using another mapper in an embbedded hash' do 26 | class Test::PriorityMapper < ROM::Mapper 27 | attribute :value, type: :integer 28 | attribute :desc 29 | end 30 | 31 | class Test::TaskMapper < ROM::Mapper 32 | attribute :title 33 | embedded :priority, type: :hash, mapper: Test::PriorityMapper 34 | end 35 | 36 | mapper = Test::TaskMapper.build 37 | relation = [ 38 | { title: 'Task One', priority: { value: '1' } }, 39 | { title: 'Task Two', priority: { value: '2' } } 40 | ] 41 | result = mapper.call(relation) 42 | 43 | expect(result).to eql([ 44 | { title: 'Task One', priority: { value: 1 } }, 45 | { title: 'Task Two', priority: { value: 2 } } 46 | ]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /core/spec/integration/mapper/setup_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Mapper Setup' do 6 | include_context 'container' 7 | 8 | before do 9 | configuration.mappers do 10 | define(:users_mapper) do 11 | attribute :id 12 | end 13 | 14 | define(:tags_mapper) do 15 | attribute :user_id 16 | attribute :label 17 | end 18 | end 19 | end 20 | 21 | it 'accessible through register_as with the same name as use in the define method' do 22 | expected_result = %i[users_mapper tags_mapper] 23 | expect(configuration.mapper_classes.map(&:register_as)).to eq expected_result 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/integration/memory/commands/create_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'rom/memory' 6 | 7 | RSpec.describe ROM::Memory::Commands::Create do 8 | include_context 'container' 9 | include_context 'users and tasks' 10 | 11 | before do 12 | configuration.relation(:users) do 13 | def by_id(id) 14 | restrict(id: id) 15 | end 16 | end 17 | configuration.commands(:users) do 18 | define(:create) 19 | end 20 | end 21 | 22 | subject(:command) { container.commands[:users].create } 23 | 24 | it_behaves_like 'a command' 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/integration/memory/commands/delete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'rom/memory' 6 | 7 | RSpec.describe ROM::Memory::Commands::Delete do 8 | include_context 'container' 9 | include_context 'users and tasks' 10 | 11 | before do 12 | configuration.relation(:users) do 13 | def by_id(id) 14 | restrict(id: id) 15 | end 16 | end 17 | configuration.commands(:users) do 18 | define(:delete) 19 | end 20 | end 21 | 22 | subject(:command) { container.commands[:users].delete } 23 | 24 | it_behaves_like 'a command' 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/integration/memory/commands/update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'rom/memory' 6 | 7 | RSpec.describe ROM::Memory::Commands::Update do 8 | include_context 'container' 9 | include_context 'users and tasks' 10 | 11 | before do 12 | configuration.relation(:users) do 13 | def by_id(id) 14 | restrict(id: id) 15 | end 16 | end 17 | configuration.commands(:users) do 18 | define(:update) 19 | end 20 | end 21 | 22 | subject(:command) { container.commands[:users].update } 23 | 24 | it_behaves_like 'a command' 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/integration/multi_repo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/memory' 5 | 6 | RSpec.describe 'Using in-memory gateways for cross-gateway access' do 7 | let(:configuration) do 8 | ROM::Configuration.new(left: :memory, right: :memory, main: :memory) 9 | end 10 | 11 | let(:container) { ROM.container(configuration) } 12 | let(:gateways) { container.gateways } 13 | 14 | it 'works' do 15 | configuration.relation(:users, gateway: :left) do 16 | def by_name(name) 17 | restrict(name: name) 18 | end 19 | end 20 | 21 | configuration.relation(:tasks, gateway: :right) 22 | 23 | configuration.relation(:users_and_tasks, gateway: :main) do 24 | def by_user(name) 25 | join(users.by_name(name), tasks) 26 | end 27 | end 28 | 29 | configuration.mappers do 30 | define(:users_and_tasks) do 31 | group tasks: [:title] 32 | end 33 | end 34 | 35 | gateways[:left][:users] << { user_id: 1, name: 'Joe' } 36 | gateways[:left][:users] << { user_id: 2, name: 'Jane' } 37 | gateways[:right][:tasks] << { user_id: 1, title: 'Have fun' } 38 | gateways[:right][:tasks] << { user_id: 2, title: 'Have fun' } 39 | 40 | user_and_tasks = container.relations[:users_and_tasks] 41 | .by_user('Jane') 42 | .map_with(:users_and_tasks) 43 | 44 | expect(user_and_tasks).to match_array([ 45 | { user_id: 2, name: 'Jane', tasks: [{ title: 'Have fun' }] } 46 | ]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /core/spec/integration/relations/default_dataset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ROM::Relation, '.dataset' do 6 | include_context 'container' 7 | 8 | it 'injects configured dataset when block was provided' do 9 | configuration.relation(:users) do 10 | dataset do |_klass| 11 | insert(id: 2, name: 'Joe') 12 | insert(id: 1, name: 'Jane') 13 | 14 | restrict(name: 'Jane') 15 | end 16 | end 17 | 18 | expect(container.relations[:users].to_a).to eql([{ id: 1, name: 'Jane' }]) 19 | end 20 | 21 | it 'yields relation class for setting custom dataset proc' do 22 | configuration.relation(:users) do 23 | schema(:users) do 24 | attribute :id, ROM::Memory::Types::Integer.meta(primary_key: true) 25 | attribute :name, ROM::Memory::Types::String 26 | end 27 | 28 | dataset do |rel_klass| 29 | insert(id: 2, name: 'Joe') 30 | insert(id: 1, name: 'Jane') 31 | 32 | order(*rel_klass.schema.primary_key.map(&:name)) 33 | end 34 | end 35 | 36 | expect(container.relations[:users].to_a).to eql([ 37 | { id: 1, name: 'Jane' }, { id: 2, name: 'Joe' } 38 | ]) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /core/spec/integration/relations/inheritance_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Inheritance relation hierarchy' do 6 | include_context 'container' 7 | 8 | before do 9 | module Test 10 | class Users < ROM::Relation[:memory] 11 | schema(:users) {} 12 | 13 | def by_email(email) 14 | restrict(email: email) 15 | end 16 | end 17 | 18 | class OtherUsers < Users 19 | schema(:other_users) {} 20 | end 21 | end 22 | 23 | configuration.register_relation(Test::Users, Test::OtherUsers) 24 | end 25 | 26 | it 'registers parent and descendant relations' do 27 | users = container.relations[:users] 28 | other_users = container.relations[:other_users] 29 | 30 | expect(users).to be_instance_of(Test::Users) 31 | expect(other_users).to be_instance_of(Test::OtherUsers) 32 | 33 | jane = { name: 'Jane', email: 'jane@doe.org' } 34 | 35 | other_users.insert(jane) 36 | 37 | expect(other_users.by_email('jane@doe.org').one).to eql(jane) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /core/spec/integration/relations/registry_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Relation registration DSL' do 6 | include_context 'container' 7 | include_context 'users and tasks' 8 | 9 | it 'allows to expose chainable relations' do 10 | configuration.relation(:tasks) do 11 | def high_priority 12 | restrict { |tuple| tuple[:priority] < 2 } 13 | end 14 | 15 | def by_title(title) 16 | restrict(title: title) 17 | end 18 | end 19 | 20 | configuration.relation(:users) do 21 | def with_tasks 22 | join(tasks) 23 | end 24 | end 25 | 26 | tasks = container.relations.tasks 27 | 28 | expect(tasks.class.name).to eql('ROM::Relation[Tasks]') 29 | expect(tasks.high_priority.inspect).to include('# 1, 'name' => 'Jane']) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /core/spec/shared/command_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples_for 'a command' do 4 | describe '#name' do 5 | it 'returns relation name' do 6 | expect(command.name).to eql(command.relation.name) 7 | end 8 | end 9 | 10 | describe '#gateway' do 11 | it 'returns relation gateway' do 12 | expect(command.gateway).to eql(command.relation.gateway) 13 | end 14 | end 15 | 16 | describe '#method_missing' do 17 | it 'forwards to relation and wraps response if it returned another relation' do 18 | if command.class.restrictable 19 | new_command = command.by_id(1) 20 | 21 | expect(new_command).to be_instance_of(command.class) 22 | expect(new_command.relation).to eql(command.by_id(1).relation) 23 | end 24 | end 25 | 26 | it 'raises error when message is not known' do 27 | expect { command.not_here }.to raise_error(NoMethodError, /not_here/) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /core/spec/shared/command_graph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'command graph' do 4 | include_context 'container' 5 | 6 | before do 7 | configuration.relation :users do 8 | def by_name(name) 9 | restrict(name: name) 10 | end 11 | end 12 | 13 | configuration.relation :tasks do 14 | def by_user_and_title(user, title) 15 | by_user(user).by_title(title) 16 | end 17 | 18 | def by_user(user) 19 | restrict(user: user) 20 | end 21 | 22 | def by_title(title) 23 | restrict(title: title) 24 | end 25 | end 26 | 27 | configuration.relation :books 28 | configuration.relation :tags 29 | 30 | configuration.commands(:users) do 31 | define(:create) do 32 | result :one 33 | end 34 | end 35 | 36 | configuration.commands(:books) do 37 | define(:create) do 38 | before :associate 39 | 40 | def associate(tuples, user) 41 | tuples.map { |t| t.merge(user: user.fetch(:name)) } 42 | end 43 | end 44 | end 45 | 46 | configuration.commands(:tags) do 47 | define(:create) do 48 | before :associate 49 | 50 | def associate(tuples, task) 51 | tuples.map { |t| t.merge(task: task.fetch(:title)) } 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /core/spec/shared/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'container' do 4 | let(:container) { ROM.container(configuration) } 5 | let(:configuration) { ROM::Configuration.new(:memory) } 6 | let(:gateway) { configuration.gateways[:default] } 7 | let(:users_relation) { container.relations[:users] } 8 | let(:tasks_relation) { container.relations[:tasks] } 9 | let(:users_dataset) { gateway.dataset(:users) } 10 | let(:tasks_dataset) { gateway.dataset(:tasks) } 11 | end 12 | -------------------------------------------------------------------------------- /core/spec/shared/enumerable_dataset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples_for 'an enumerable dataset' do 4 | subject(:dataset) { klass.new(data) } 5 | 6 | let(:data) do 7 | [{ 'name' => 'Jane' }, { 'name' => 'Joe' }] 8 | end 9 | 10 | describe '#each' do 11 | it 'yields tuples through row_proc' do 12 | result = [] 13 | 14 | dataset.each do |tuple| 15 | result << tuple 16 | end 17 | 18 | expect(result).to match_array([{ name: 'Jane' }, { name: 'Joe' }]) 19 | end 20 | end 21 | 22 | describe '#to_a' do 23 | it 'casts dataset to an array' do 24 | expect(dataset.to_a).to eql([{ name: 'Jane' }, { name: 'Joe' }]) 25 | end 26 | end 27 | 28 | describe '#find_all' do 29 | it 'yields tuples through row_proc' do 30 | result = dataset.find_all { |tuple| tuple[:name] == 'Jane' } 31 | 32 | expect(result).to be_instance_of(klass) 33 | expect(result).to match_array([{ name: 'Jane' }]) 34 | end 35 | end 36 | 37 | describe '#kind_of?' do 38 | it 'does not forward to data object' do 39 | expect(dataset).to be_kind_of(klass) 40 | end 41 | end 42 | 43 | describe '#instance_of?' do 44 | it 'does not forward to data object' do 45 | expect(dataset).to be_instance_of(klass) 46 | end 47 | end 48 | 49 | describe '#is_a?' do 50 | it 'does not forward to data object' do 51 | expect(dataset.is_a?(klass)).to be(true) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /core/spec/shared/gateway_only.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'gateway only' do 4 | let(:gateway) { ROM::Memory::Gateway.new } 5 | 6 | let(:users_dataset) { gateway.dataset(:users) } 7 | let(:tasks_dataset) { gateway.dataset(:tasks) } 8 | end 9 | -------------------------------------------------------------------------------- /core/spec/shared/materializable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples_for 'materializable relation' do 4 | describe '#each' do 5 | it 'yields objects' do 6 | count = relation.to_a.size 7 | result = [] 8 | 9 | relation.each do |object| 10 | result << object 11 | end 12 | 13 | expect(result.count).to eql(count) 14 | end 15 | 16 | it 'returns enumerator when block is not provided' do 17 | expect(relation.each.to_a).to eql(relation.to_a) 18 | end 19 | end 20 | 21 | describe '#one' do 22 | it 'returns one tuple' do 23 | expect(relation.one).to be_instance_of(Hash) 24 | end 25 | end 26 | 27 | describe '#first' do 28 | it 'returns first tuple' do 29 | expect(relation.first).to be_instance_of(Hash) 30 | end 31 | end 32 | 33 | describe '#call' do 34 | it 'materializes relation' do 35 | expect(relation.call).to be_instance_of(ROM::Relation::Loaded) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /core/spec/shared/no_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory' 4 | 5 | RSpec.shared_context 'no container' do 6 | let(:gateway) { ROM::Memory::Gateway.new } 7 | 8 | let(:users_dataset) { gateway.dataset(:users) } 9 | let(:tasks_dataset) { gateway.dataset(:tasks) } 10 | 11 | let(:users_relation) do 12 | Class.new(ROM::Memory::Relation).new(users_dataset) 13 | end 14 | 15 | let(:tasks_relation) do 16 | Class.new(ROM::Memory::Relation).new(tasks_dataset) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /core/spec/shared/one_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples_for 'a relation that returns one tuple' do 4 | describe '#one' do 5 | it 'returns first tuple' do 6 | users_relation.delete(name: 'Joe', email: 'joe@doe.org') 7 | expect(relation.one).to eql(name: 'Jane', email: 'jane@doe.org') 8 | end 9 | 10 | it 'raises error when there is more than one tuple' do 11 | expect { relation.one }.to raise_error(ROM::TupleCountMismatchError) 12 | end 13 | end 14 | 15 | describe '#one!' do 16 | it 'returns first tuple' do 17 | users_relation.delete(name: 'Joe', email: 'joe@doe.org') 18 | expect(relation.one!).to eql(name: 'Jane', email: 'jane@doe.org') 19 | end 20 | 21 | it 'raises error when there is no tuples' do 22 | users_relation.delete(name: 'Jane', email: 'jane@doe.org') 23 | users_relation.delete(name: 'Joe', email: 'joe@doe.org') 24 | 25 | expect { relation.one! }.to raise_error(ROM::TupleCountMismatchError) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /core/spec/shared/users_and_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'users and tasks' do 4 | before do 5 | users_dataset.insert(name: 'Joe', email: 'joe@doe.org') 6 | users_dataset.insert(name: 'Jane', email: 'jane@doe.org') 7 | 8 | tasks_dataset.insert(name: 'Joe', title: 'be nice', priority: 1) 9 | tasks_dataset.insert(name: 'Joe', title: 'sleep well', priority: 2) 10 | tasks_dataset.insert(name: 'Jane', title: 'be cool', priority: 2) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /core/spec/support/constant_leak_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Finds leaking constants created during ROM specs 4 | module ConstantLeakFinder 5 | def self.find(example) 6 | constants = Object.constants 7 | 8 | example.run 9 | 10 | added_constants = (Object.constants - constants) 11 | added = added_constants.map(&Object.method(:const_get)) 12 | if added.any? { |mod| mod.ancestors.map(&:name).grep(/\AROM/).any? } 13 | raise "Leaking constants: #{added_constants.inspect}" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /core/spec/support/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/inflector' 4 | 5 | module SchemaHelpers 6 | def define_schema(source, attrs) 7 | ROM::Schema.define( 8 | source, 9 | attributes: attrs.map do |name, id| 10 | { 11 | type: define_type(id, source: source), 12 | options: { name: name } 13 | } 14 | end 15 | ) 16 | end 17 | 18 | def define_type(id, **meta) 19 | ROM::Types.const_get(id).meta(meta) 20 | end 21 | 22 | # @todo Use this method consistently in all the test suite 23 | def define_attribute(id, opts, meta = {}) 24 | type = define_type(id, **meta) 25 | ROM::Attribute.new(type, **opts) 26 | end 27 | 28 | def define_attr_info(id, opts, meta = {}) 29 | ROM::Schema.build_attribute_info( 30 | define_type(id, **meta), 31 | **opts 32 | ) 33 | end 34 | 35 | def build_assoc(type, *args, **kwargs) 36 | klass = ROM::Inflector.classify(type) 37 | definition = ROM::Associations::Definitions.const_get(klass).new(*args, **kwargs) 38 | ROM::Memory::Associations.const_get(definition.type).new(definition, relations) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /core/spec/support/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-types' 4 | 5 | module Types 6 | include Dry.Types(default: :nominal) 7 | end 8 | -------------------------------------------------------------------------------- /core/spec/test/memory_repository_lint_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/core' 4 | require 'rom/memory' 5 | require 'rom/lint/test' 6 | 7 | require 'minitest/autorun' 8 | 9 | class MemoryRepositoryLintTest < Minitest::Test 10 | include ROM::Lint::TestGateway 11 | 12 | def setup 13 | @gateway = ROM::Memory::Gateway 14 | @identifier = :memory 15 | end 16 | 17 | def gateway_instance 18 | ROM::Memory::Gateway.new 19 | end 20 | end 21 | 22 | class MemoryDatasetLintTest < Minitest::Test 23 | include ROM::Lint::TestEnumerableDataset 24 | 25 | def setup 26 | @data = [{ name: 'Jane', age: 24 }, { name: 'Joe', age: 25 }] 27 | @dataset = ROM::Memory::Dataset.new(@data) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/spec/unit/rom/association_set_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::AssociationSet do 4 | subject(:set) { ROM::AssociationSet[:projects].build(elements) } 5 | 6 | describe '.[]' do 7 | it 'builds a class with the provided identifier' do 8 | klass = ROM::AssociationSet[:users] 9 | 10 | expect(klass.name).to eql('ROM::AssociationSet[:users]') 11 | end 12 | 13 | it 'caches the class' do 14 | expect(ROM::AssociationSet[:users]).to be(ROM::AssociationSet[:users]) 15 | end 16 | end 17 | 18 | describe '#[]' do 19 | let(:elements) { { users: users, post: posts } } 20 | 21 | let(:users) { double(:users, aliased?: false) } 22 | let(:posts) { double(:posts, aliased?: true, as: :post, name: :posts) } 23 | 24 | it 'fetches association' do 25 | expect(set[:users]).to be users 26 | end 27 | 28 | it 'fetches association by alias' do 29 | expect(set[:post]).to be posts 30 | end 31 | 32 | it 'fetches association by canonical name' do 33 | expect(set[:posts]).to be posts 34 | end 35 | 36 | it 'throws exception on missing association' do 37 | expect { set[:labels] }.to raise_error( 38 | ROM::ElementNotFoundError, 39 | ":labels doesn't exist in ROM::AssociationSet[:projects] registry" 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /core/spec/unit/rom/attribute/method_missing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | require 'rom/attribute' 5 | 6 | RSpec.describe ROM::Attribute, '#method_missing' do 7 | subject(:attribute) do 8 | ROM::Attribute.new(type, name: :foo) 9 | end 10 | 11 | context 'with a plain definition' do 12 | let(:type) do 13 | ROM::Types::Integer 14 | end 15 | 16 | it 'forwards to its type' do 17 | expect(attribute.primitive).to be(Integer) 18 | end 19 | 20 | it 'raises when unknown method was called' do 21 | expect { attribute.not_here }.to raise_error(NoMethodError, /not_here/) 22 | end 23 | end 24 | 25 | context 'with a type that can return new instances of its class' do 26 | let(:type) do 27 | ROM::Types::Integer.default(1) 28 | end 29 | 30 | it 'returns a new attribute if forwarded method returned a new type' do 31 | new_attribute = attribute.default(2) 32 | 33 | expect(new_attribute[]).to be(2) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /core/spec/unit/rom/attribute/optional_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | require 'rom/attribute' 5 | 6 | RSpec.describe ROM::Attribute, '#optional' do 7 | subject(:attribute) do 8 | ROM::Attribute.new(ROM::Types::Integer, name: :id).meta(read: ROM::Types::Coercible::Integer) 9 | end 10 | 11 | it 'transforms read type' do 12 | expect(attribute.optional.to_read_type['1']).to eql(1) 13 | expect(attribute.optional.to_read_type[nil]).to be_nil 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /core/spec/unit/rom/attribute/to_ast_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/types' 4 | require 'rom/attribute' 5 | 6 | RSpec.describe ROM::Attribute, '#to_ast' do 7 | subject(:attribute) { ROM::Attribute.new(ROM::Types::Integer, name: :id) } 8 | 9 | types = [ 10 | ROM::Types::Integer, 11 | ROM::Types::Strict::Integer, 12 | ROM::Types::Strict::Integer.optional 13 | ] 14 | 15 | to_attr = -> type { ROM::Attribute.new(type, name: :id) } 16 | 17 | types.each do |type| 18 | specify do 19 | expect( 20 | to_attr.(type).to_ast 21 | ).to eql([:attribute, [:id, type.to_ast, alias: nil]]) 22 | end 23 | end 24 | 25 | example 'wrapped type' do 26 | expect(attribute.wrapped(:users).to_ast) 27 | .to eql([:attribute, [:id, ROM::Types::Integer.to_ast, 28 | wrapped: true, alias: :users_id]]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /core/spec/unit/rom/cache_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/cache' 4 | 5 | RSpec.describe ROM::Cache do 6 | subject(:cache) { ROM::Cache.new } 7 | 8 | describe '#fetch_or_store' do 9 | it 'returns existing object' do 10 | obj = 'foo' 11 | 12 | expect(cache.fetch_or_store(obj) { obj }) 13 | expect(cache.fetch_or_store(obj)).to be(obj) 14 | end 15 | end 16 | 17 | describe '#namespaced' do 18 | it 'returns a namespaced cache' do 19 | namespaced = cache.namespaced(:foo) 20 | 21 | expect(namespaced).to be_instance_of(ROM::Cache::Namespaced) 22 | expect(namespaced).to be(cache.namespaced(:foo)) 23 | end 24 | end 25 | 26 | context 'namespace cache' do 27 | describe '#fetch_or_store' do 28 | it 'returns existing object' do 29 | namespaced = cache.namespaced('stuff') 30 | obj = 'foo' 31 | 32 | expect(namespaced.fetch_or_store(obj) { obj }) 33 | expect(namespaced.fetch_or_store(obj)).to be(obj) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/spec/unit/rom/configurable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ROM::Configurable do 6 | let(:klass) { Class.new { include ROM::Configurable } } 7 | let(:object) { klass.new } 8 | let(:config) { object.config } 9 | 10 | it 'exposes the config property' do 11 | expect { config }.not_to raise_error 12 | end 13 | 14 | it 'is configurable via block' do 15 | object.configure do |config| 16 | config.sql.infer_relations = false 17 | end 18 | 19 | expect(config.sql.infer_relations).to be(false) 20 | end 21 | 22 | context ROM::Configurable::Config do 23 | it 'can be traversed via dot syntax' do 24 | config.sql.infer_relations = false 25 | expect(config.sql.infer_relations).to be(false) 26 | end 27 | 28 | it 'can be traversed via bracket syntax' do 29 | config[:sql].infer_relations = false 30 | 31 | expect(config[:sql][:infer_relations]).to be(false) 32 | expect(config).to respond_to(:sql) 33 | expect(config.sql.infer_relations).to be(false) 34 | end 35 | 36 | it 'freezes properly' do 37 | config.freeze 38 | 39 | expect { config.sql.infer_relations = false }.to raise_error(NoMethodError) 40 | end 41 | 42 | it 'handles unset keys when frozen' do 43 | config.sql.infer_relations = false 44 | config.freeze 45 | 46 | expect(config.other).to be(nil) 47 | expect(config.key?(:other)).to be(false) 48 | expect(config.key?(:sql)).to be(true) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /core/spec/unit/rom/enumerable_dataset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/enumerable_dataset' 4 | 5 | RSpec.describe ROM::EnumerableDataset do 6 | let(:klass) do 7 | Class.new do 8 | include ROM::EnumerableDataset 9 | 10 | def self.row_proc 11 | -> i { i.transform_keys(&:to_sym) } 12 | end 13 | end 14 | end 15 | 16 | it_behaves_like 'an enumerable dataset' 17 | end 18 | -------------------------------------------------------------------------------- /core/spec/unit/rom/mapper/model_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ROM::Mapper::ModelDSL do 6 | describe '#model' do 7 | it 'calls the builder with non-excluded attributes only' do 8 | definition_class = Class.new do 9 | include ROM::Mapper::ModelDSL 10 | 11 | def initialize 12 | @attributes = [[:name], [:title, exclude: true]] 13 | @builder = ->(attrs) { Struct.new(*attrs) } 14 | end 15 | end 16 | model_instance = definition_class.new.model.new 17 | expect(model_instance).to respond_to(:name) 18 | expect(model_instance).to_not respond_to(:title) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /core/spec/unit/rom/mapper_compiler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::MapperCompiler, '#call' do 4 | subject(:mapper_compiler) do 5 | Class.new(ROM::MapperCompiler) do 6 | mapper_options(reject_keys: true) 7 | end.new 8 | end 9 | 10 | let(:ast) do 11 | ROM::Relation.new([], schema: define_schema(:users, id: :Integer, name: :String)).to_ast 12 | end 13 | 14 | let(:data) do 15 | [{ id: 1, name: 'Jane', email: 'jane@doe.org' }] 16 | end 17 | 18 | it 'sets mapper options' do 19 | mapper = mapper_compiler.call(ast) 20 | 21 | expect(mapper.call(data)).to eql([{ id: 1, name: 'Jane' }]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memoizable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/support/memoizable' 4 | 5 | RSpec.describe ROM::Memoizable, '.memoize' do 6 | subject(:object) do 7 | Class.new do 8 | include ROM::Memoizable 9 | 10 | def foo 11 | %w[a ab abc].max 12 | end 13 | memoize :foo 14 | 15 | def bar(_arg) 16 | { a: '1', b: '2' } 17 | end 18 | memoize :bar 19 | end.new 20 | end 21 | 22 | it 'memoizes method return value' do 23 | expect(object.foo).to be(object.foo) 24 | end 25 | 26 | it 'memoizes method return value with an arg' do 27 | expect(object.bar(:a)).to be(object.bar(:a)) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memory/commands_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/memory' 5 | 6 | RSpec.describe ROM::Memory::Commands do 7 | let(:relation) do 8 | Class.new(ROM::Relation[:memory]) do 9 | schema do 10 | attribute :id, ROM::Memory::Types::Integer 11 | attribute :name, ROM::Memory::Types::String 12 | end 13 | end.new(ROM::Memory::Dataset.new([])) 14 | end 15 | 16 | describe 'Create' do 17 | subject(:command) { ROM::Commands::Create[:memory].build(relation) } 18 | 19 | describe '#call' do 20 | it 'uses default input handler' do 21 | result = command.call([id: 1, name: 'Jane', haha: 'oops']) 22 | 23 | expect(result).to eql([{ id: 1, name: 'Jane' }]) 24 | end 25 | end 26 | end 27 | 28 | describe 'Update' do 29 | subject(:command) { ROM::Commands::Update[:memory].build(relation) } 30 | 31 | before do 32 | relation.insert(id: 1, name: 'Jane') 33 | end 34 | 35 | describe '#call' do 36 | it 'uses default input handler' do 37 | result = command 38 | .new(relation.restrict(id: 1)) 39 | .call(name: 'Jane Doe', haha: 'oops') 40 | 41 | expect(result).to eql([{ id: 1, name: 'Jane Doe' }]) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memory/dataset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/lint/spec' 5 | 6 | require 'rom/memory/dataset' 7 | 8 | RSpec.describe ROM::Memory::Dataset do 9 | subject(:dataset) { ROM::Memory::Dataset.new(data) } 10 | 11 | let(:data) do 12 | [ 13 | { name: 'Jane', email: 'jane@doe.org', age: 10 }, 14 | { name: 'Jade', email: 'jade@doe.org', age: 11 }, 15 | { name: 'Joe', email: 'joe@doe.org', age: 12 } 16 | ] 17 | end 18 | 19 | it_behaves_like 'a rom enumerable dataset' 20 | 21 | describe 'subclassing' do 22 | it 'supports options' do 23 | descendant = Class.new(ROM::Memory::Dataset) do 24 | option :path 25 | end 26 | 27 | dataset = descendant.new([1, 2, 3], path: '/data') 28 | 29 | expect(dataset.to_a).to eql([1, 2, 3]) 30 | expect(dataset.path).to eql('/data') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memory/gateway_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/lint/spec' 5 | require 'rom/memory' 6 | 7 | RSpec.describe ROM::Memory::Gateway do 8 | let(:gateway) { ROM::Memory::Gateway } 9 | let(:uri) { nil } 10 | 11 | it_behaves_like 'a rom gateway' do 12 | let(:identifier) { :memory } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memory/inheritance_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory' 4 | 5 | RSpec.describe ROM::Memory::Relation, '.inherited' do 6 | subject(:relation) do 7 | Class.new(ROM::Relation[:test]) do 8 | schema(:users) do 9 | attribute :name, ROM::Types::String 10 | end 11 | end.new([]) 12 | end 13 | 14 | before do 15 | module Test 16 | module AnotherAdapter 17 | class Relation < ROM::Memory::Relation 18 | adapter :test 19 | end 20 | end 21 | end 22 | 23 | ROM.register_adapter(:test, Test::AnotherAdapter) 24 | end 25 | 26 | after do 27 | ROM.adapters.delete(:test) 28 | end 29 | 30 | it 'extends subclass with core methods' do 31 | expect(relation.name.to_sym).to be(:users) 32 | expect(relation.schema.map(&:name)).to eql(%i[name]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /core/spec/unit/rom/memory/storage_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rom/memory/storage' 5 | 6 | RSpec.describe ROM::Memory::Storage do 7 | describe 'thread safe' do 8 | let(:threads) { 4 } 9 | let(:operations) { 5000 } 10 | 11 | describe 'data' do 12 | it 'create datasets properly' do 13 | storage = ROM::Memory::Storage.new 14 | 15 | threaded_operations do |thread, operation| 16 | key = "#{thread}:#{operation}" 17 | storage.create_dataset(key) 18 | end 19 | 20 | expect(storage.size).to eql(threads * operations) 21 | end 22 | end 23 | 24 | describe 'dataset' do 25 | it 'inserts data in proper order' do 26 | storage = ROM::Memory::Storage.new 27 | dataset = storage.create_dataset(:ary) 28 | 29 | threaded_operations do 30 | dataset << :data 31 | end 32 | 33 | expect(dataset.size).to eql(threads * operations) 34 | end 35 | end 36 | 37 | def threaded_operations 38 | threads.times.map { |thread| 39 | Thread.new do 40 | operations.times do |operation| 41 | yield thread, operation 42 | end 43 | end 44 | }.each(&:join) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /core/spec/unit/rom/plugins/relation/instrumentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/core' 4 | require 'rom/memory' 5 | 6 | RSpec.describe ROM::Plugins::Relation::Instrumentation do 7 | before do 8 | described_class.mixin Module.new 9 | end 10 | 11 | subject(:relation) do 12 | relation_class.new(dataset, notifications: notifications) 13 | end 14 | 15 | let(:dataset) do 16 | double(:dataset) 17 | end 18 | 19 | let(:relation_class) do 20 | Class.new(ROM::Memory::Relation) do 21 | schema(:users) do 22 | attribute :name, ROM::Types::String 23 | end 24 | 25 | use :instrumentation 26 | 27 | instrument def all 28 | self 29 | end 30 | end 31 | end 32 | 33 | let(:notifications) { spy(:notifications) } 34 | 35 | it 'uses notifications API when materializing a relation' do 36 | relation.to_a 37 | 38 | expect(notifications).to have_received(:instrument).with(:memory, name: :users) 39 | end 40 | 41 | it 'instruments custom methods' do 42 | relation.all 43 | 44 | expect(notifications).to have_received(:instrument).with(:memory, name: :users) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /core/spec/unit/rom/plugins/schema/timestamps_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Plugins::Schema::Timestamps do 4 | let(:relation) { ROM::Relation::Name[:users] } 5 | 6 | let(:schema_dsl) do 7 | ROM::Schema::DSL.new(relation) 8 | end 9 | 10 | subject(:schema) { schema_dsl.call } 11 | 12 | it 'adds timestamp attributes' do 13 | ts_attribute = lambda do |name| 14 | ROM::Attribute.new(ROM::Types::Time.meta(source: relation), name: name) 15 | end 16 | 17 | schema_dsl.use :timestamps 18 | 19 | expect(schema[:created_at]).to eql(ts_attribute.(:created_at)) 20 | expect(schema[:updated_at]).to eql(ts_attribute.(:updated_at)) 21 | end 22 | 23 | it 'supports custom names' do 24 | schema_dsl.use :timestamps 25 | schema_dsl.timestamps :created_on, :updated_on 26 | 27 | expect(schema.to_h.keys).to eql(%i[created_on updated_on]) 28 | end 29 | 30 | it 'supports custom types' do 31 | schema_dsl.use :timestamps, type: ROM::Types::Date 32 | dt_attribute = lambda do |name| 33 | ROM::Attribute.new(ROM::Types::Date.meta(source: relation), name: name) 34 | end 35 | 36 | expect(schema[:created_at]).to eql(dt_attribute.(:created_at)) 37 | expect(schema[:updated_at]).to eql(dt_attribute.(:updated_at)) 38 | end 39 | 40 | it 'supports custom names with options' do 41 | schema_dsl.use :timestamps, attributes: %i[created_on updated_on] 42 | 43 | expect(schema.to_h.keys).to eql(%i[created_on updated_on]) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/as_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#map_to' do 6 | subject(:relation) do 7 | ROM::Relation.new([], name: ROM::Relation::Name[:users]) 8 | end 9 | 10 | it 'returns a new relation with an aliased name' do 11 | expect(relation.as(:people).name).to eql(ROM::Relation::Name[:users].as(:people)) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/attribute_reader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory' 4 | 5 | RSpec.describe ROM::Relation, '#[]' do 6 | it 'defines a canonical schema for a relation' do 7 | class Test::Users < ROM::Relation[:memory] 8 | schema do 9 | attribute :id, Types::Integer 10 | attribute :name, Types::String 11 | end 12 | end 13 | 14 | relation = Test::Users.new([]) 15 | 16 | expect(relation[:id]).to be(Test::Users.schema[:id]) 17 | expect(relation[:name]).to be(Test::Users.schema[:name]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/class_interface/relation_name_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '.relation_name' do 6 | context 'when schema is defined' do 7 | subject(:relation_class) do 8 | Class.new(ROM::Relation) do 9 | schema(:users) do 10 | attribute :name, ROM::Types::String 11 | end 12 | end 13 | end 14 | 15 | it 'returns relation name configured by schema' do 16 | expect(relation_class.relation_name).to eql(ROM::Relation::Name[:users]) 17 | end 18 | end 19 | 20 | context 'when schema is not defined' do 21 | subject(:relation_class) do 22 | Class.new(ROM::Relation) 23 | end 24 | 25 | it 'raises error' do 26 | expect { relation_class.relation_name }.to raise_error(ROM::MissingSchemaError) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, [tasks]) 6 | end 7 | 8 | let(:users) do 9 | Class.new(ROM::Relation) do 10 | auto_map false 11 | end.new([]) 12 | end 13 | 14 | let(:tasks) do 15 | Class.new(ROM::Relation) do 16 | auto_map false 17 | 18 | def call(_users) 19 | ROM::Relation::Loaded.new(self) 20 | end 21 | end.new([]) 22 | end 23 | 24 | describe '#call' do 25 | it 'materializes relations' do 26 | result = relation.call 27 | 28 | expect(result).to be_instance_of(ROM::Relation::Loaded) 29 | expect(result.source).to be(relation) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/combine_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/combined' 4 | 5 | RSpec.describe ROM::Relation::Combined, '#combine' do 6 | subject(:relation) do 7 | ROM::Relation::Combined.new(users, [tasks]) 8 | end 9 | 10 | let(:users) do 11 | ROM::Relation.new([], name: ROM::Relation::Name[:users]) 12 | end 13 | 14 | let(:tasks) do 15 | ROM::Relation.new([], name: ROM::Relation::Name[:tasks]) 16 | end 17 | 18 | let(:posts) do 19 | ROM::Relation.new([], name: ROM::Relation::Name[:posts]) 20 | end 21 | 22 | let(:tags) do 23 | ROM::Relation.new([], name: ROM::Relation::Name[:tags]) 24 | end 25 | 26 | it 'returns another combined relation with nodes appended' do 27 | expect(relation.root).to receive(:nodes).with(posts, tags).and_return([posts, tags]) 28 | 29 | new_relation = relation.combine(posts, tags) 30 | 31 | expect(new_relation.root).to be(users) 32 | expect(new_relation.nodes).to eql([tasks, posts, tags]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/command_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/combined' 4 | 5 | RSpec.describe ROM::Relation::Combined, '#command' do 6 | subject(:relation) do 7 | ROM::Relation::Combined.new(users, [tasks]) 8 | end 9 | 10 | let(:users) do 11 | ROM::Relation.new([]) 12 | end 13 | 14 | let(:tasks) do 15 | ROM::Relation.new([]) 16 | end 17 | 18 | it 'raises when type is not :create' do 19 | expect { relation.command(:update) } 20 | .to raise_error( 21 | NotImplementedError, 22 | "ROM::Relation::Combined#command doesn't work with :update command type yet" 23 | ) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/graph_method_missing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, []) 6 | end 7 | 8 | let(:users) do 9 | Class.new(ROM::Relation) do 10 | def by_name(_name) 11 | self 12 | end 13 | end.new([]) 14 | end 15 | 16 | describe '#method_missing' do 17 | it 'responds to the root methods' do 18 | expect(relation).to respond_to(:by_name) 19 | end 20 | 21 | it 'forwards methods to the root and decorates response' do 22 | expect(relation.by_name('Jane')).to be_instance_of(ROM::Relation::Combined) 23 | end 24 | 25 | it 'forwards methods to the root and decorates curried response' do 26 | expect(relation.by_name).to be_instance_of(ROM::Relation::Combined) 27 | end 28 | 29 | it 'returns original response from the root' do 30 | expect(relation.name).to be(users.name) 31 | end 32 | 33 | it 'raises no method error' do 34 | expect { relation.not_here }.to raise_error(NoMethodError, /not_here/) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/graph_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, []) 6 | end 7 | 8 | let(:users) do 9 | Class.new(ROM::Relation) do 10 | def by_name(_name) 11 | self 12 | end 13 | end.new([]) 14 | end 15 | 16 | describe '#graph?' do 17 | it 'returns true' do 18 | expect(relation).to be_graph 19 | end 20 | 21 | it 'returns true when curried' do 22 | expect(relation.by_name).to be_graph 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/map_to_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/combined' 4 | 5 | RSpec.describe ROM::Relation::Combined, '#map_to' do 6 | subject(:relation) do 7 | ROM::Relation::Combined.new(users, [tasks.for_users]) 8 | end 9 | 10 | let(:users) do 11 | Class.new(ROM::Relation) do 12 | auto_map false 13 | end.new([{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]) 14 | end 15 | 16 | let(:tasks) do 17 | Class.new(ROM::Relation) do 18 | auto_map false 19 | 20 | def for_users(users) 21 | dataset.select { |t| users.pluck(:id).include?(t[:user_id]) } 22 | end 23 | end.new([{ user_id: 2, title: "John's Task" }, { user_id: 1, name: "Jane's Task" }]) 24 | end 25 | 26 | let(:model) do 27 | Class.new 28 | end 29 | 30 | it 'returns a new graph with custom model' do 31 | expect(relation.map_to(model).meta[:model]).to be(model) 32 | end 33 | 34 | it 'maintains nodes' do 35 | expect(relation.map_to(model).nodes).to eql(relation.nodes) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/map_with_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/combined' 4 | 5 | RSpec.describe ROM::Relation::Combined, '#map_with' do 6 | subject(:relation) do 7 | ROM::Relation::Combined.new( 8 | users, [tasks.to_node(:tasks, type: :many, keys: { id: :user_id }).for_users] 9 | ) 10 | end 11 | 12 | let(:users) do 13 | ROM::Relation.new([{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }], mappers: mapper_registry) 14 | end 15 | 16 | let(:tasks) do 17 | Class.new(ROM::Relation) do 18 | def for_users(users) 19 | new(dataset.select { |t| users.pluck(:id).include?(t[:user_id]) }) 20 | end 21 | 22 | def to_node(_name, type:, keys:) 23 | with(meta: { combine_name: :tasks, combine_type: type, keys: keys }) 24 | end 25 | end.new([{ user_id: 2, title: "John's Task" }, { user_id: 1, title: "Jane's Task" }]) 26 | end 27 | 28 | let(:mapper_registry) { ROM::MapperRegistry.build(mappers) } 29 | 30 | let(:mappers) do 31 | { task_list: lambda { |users| 32 | users.map do |u| 33 | h = u.merge(task_list: u[:tasks].map { |t| t[:title] }) 34 | h.delete(:tasks) 35 | h 36 | end 37 | } } 38 | end 39 | 40 | it 'returns a new graph with custom model' do 41 | expect(relation.map_with(:task_list).to_a) 42 | .to eql([ 43 | { id: 1, name: 'Jane', task_list: ["Jane's Task"] }, 44 | { id: 2, name: 'John', task_list: ["John's Task"] } 45 | ]) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/materializable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined do 4 | include_context 'gateway only' 5 | include_context 'users and tasks' 6 | 7 | def t(*args) 8 | ROM::Processor::Transproc::Functions[*args] 9 | end 10 | 11 | let(:users) do 12 | Class.new(ROM::Memory::Relation) do 13 | auto_map false 14 | 15 | def by_name(name) 16 | restrict(name: name) 17 | end 18 | end.new(users_dataset) 19 | end 20 | 21 | let(:tasks) do 22 | Class.new(ROM::Memory::Relation) do 23 | auto_map false 24 | 25 | def for_users(_users) 26 | self 27 | end 28 | 29 | def by_title(title) 30 | restrict(title: title) 31 | end 32 | end.new(tasks_dataset) 33 | end 34 | 35 | it_behaves_like 'materializable relation' do 36 | let(:mapper) do 37 | t(:combine, [[:tasks, name: :name]]) 38 | end 39 | 40 | let(:relation) do 41 | ROM::Relation::Combined.new(users.by_name('Jane'), [tasks.for_users]) >> mapper 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/method_missing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined, '#method_missing' do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, [tasks]) 6 | end 7 | 8 | let(:users) do 9 | Class.new(ROM::Relation) do 10 | def add_node(node) 11 | ROM::Relation::Combined.new(self, [node]) 12 | end 13 | end.new([]) 14 | end 15 | 16 | let(:tasks) do 17 | Class.new(ROM::Relation).new([], name: ROM::Relation::Name[:tasks]) 18 | end 19 | 20 | let(:posts) do 21 | Class.new(ROM::Relation).new([], name: ROM::Relation::Name[:posts]) 22 | end 23 | 24 | describe 'forwards to the root' do 25 | context 'when return value is another combined relation' do 26 | it 'merges nodes' do 27 | result = relation.add_node(posts) 28 | 29 | expect(result).to be_instance_of(ROM::Relation::Combined) 30 | expect(result.root).to be(users) 31 | expect(result.nodes).to eql([tasks, posts]) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/to_a_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined, '#to_a' do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, [tasks.for_users]) 6 | end 7 | 8 | let(:users) do 9 | Class.new(ROM::Relation) do 10 | auto_map false 11 | end.new([{ id: 1, name: 'Jane' }, { id: 2, name: 'John' }]) 12 | end 13 | 14 | let(:tasks) do 15 | Class.new(ROM::Relation) do 16 | auto_map false 17 | 18 | def for_users(users) 19 | dataset.select { |t| users.pluck(:id).include?(t[:user_id]) } 20 | end 21 | end.new([{ user_id: 2, title: "John's Task" }, { user_id: 1, name: "Jane's Task" }]) 22 | end 23 | 24 | it 'coerces to an array' do 25 | expect(relation.to_a).to match_array([users.call, [tasks.for_users(users.call)]]) 26 | end 27 | 28 | it 'returns empty arrays when left was empty' do 29 | empty_users = users.new([]) 30 | 31 | expect(relation.new([]).to_a) 32 | .to eql([empty_users.call, [ROM::Relation::Loaded.new(tasks.for_users, [])]]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/with_nodes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Relation::Combined do 4 | subject(:relation) do 5 | ROM::Relation::Combined.new(users, [tasks]) 6 | end 7 | 8 | let(:users) do 9 | ROM::Relation.new([]) 10 | end 11 | 12 | let(:tasks) do 13 | ROM::Relation.new([]) 14 | end 15 | 16 | let(:posts) do 17 | ROM::Relation.new([]) 18 | end 19 | 20 | describe '#with_nodes' do 21 | it 'returns a new graph with new nodes' do 22 | new_graph = relation.with_nodes([posts]) 23 | 24 | expect(new_graph.nodes[0]).to eql(posts) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/combined/wrap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/combined' 4 | 5 | RSpec.describe ROM::Relation::Combined, '#wrap' do 6 | subject(:relation) do 7 | ROM::Relation::Combined.new(users, [tasks]) 8 | end 9 | 10 | let(:users) do 11 | ROM::Relation.new([]) 12 | end 13 | 14 | let(:tasks) do 15 | ROM::Relation.new([]) 16 | end 17 | 18 | let(:posts) do 19 | ROM::Relation.new([]) 20 | end 21 | 22 | it 'returns a new graph with new root as a wrap relation' do 23 | posts_assoc = double(:posts_assoc) 24 | allow(users.schema).to receive(:associations).and_return(posts: posts_assoc) 25 | 26 | expect(posts_assoc).to receive(:wrap).and_return(posts) 27 | 28 | new_graph = relation.wrap(:posts) 29 | 30 | expect(new_graph.root).to be_wrap 31 | expect(new_graph.nodes).to eql(relation.nodes) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/command_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#command' do 6 | subject(:relation) do 7 | ROM::Relation.new([], name: ROM::Relation::Name[:users], commands: commands, auto_map: false) 8 | end 9 | 10 | let(:commands) do 11 | {} 12 | end 13 | 14 | context 'when command is already registered' do 15 | before do 16 | commands[:my_command] = custom_command 17 | end 18 | 19 | context 'when command is not restrictible' do 20 | let(:custom_command) do 21 | double(:command, restrictible?: false) 22 | end 23 | 24 | it 'returns the command if it exists in the registry already' do 25 | expect(relation.command(:my_command)).to be(custom_command) 26 | end 27 | end 28 | 29 | context 'when command is restrictible' do 30 | let(:custom_command) do 31 | double(:command, restrictible?: true) 32 | end 33 | 34 | it 'returns the command if it exists in the registry already' do 35 | expect(custom_command).to receive(:new).with(relation).and_return(custom_command) 36 | 37 | expect(relation.command(:my_command)).to be(custom_command) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/eager_load_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#eager_load' do 6 | let(:users) do 7 | ROM::Relation.new([], name: ROM::Relation::Name[:users]) 8 | end 9 | 10 | let(:tasks) do 11 | ROM::Relation.new([], name: ROM::Relation::Name[:tasks]) 12 | end 13 | let(:users_assocs_set) do 14 | { tasks: tasks_assoc } 15 | end 16 | 17 | let(:tasks_assoc) do 18 | double(:tasks_assoc, override?: override) 19 | end 20 | 21 | before do 22 | allow(users.schema).to receive(:associations).and_return(users_assocs_set) 23 | end 24 | 25 | context 'when assocs is not set to override default view' do 26 | let(:override) { false } 27 | 28 | it 'returns an curried relation for eager loading' do 29 | expect(tasks_assoc).to receive(:prepare).with(users).and_return(tasks) 30 | 31 | relation = users.eager_load(tasks_assoc) 32 | 33 | expect(relation).to be_curried 34 | expect(relation.relation).to be(tasks) 35 | end 36 | end 37 | 38 | context 'when assocs is set to override default view' do 39 | let(:override) { true } 40 | 41 | it 'returns an curried relation for eager loading' do 42 | expect(tasks_assoc).to receive(:prepare).with(users).and_return(tasks) 43 | expect(tasks).to receive(:call).with(tasks_assoc).and_return(tasks) 44 | 45 | relation = users.eager_load(tasks_assoc) 46 | 47 | expect(relation).to be(tasks) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/foreign_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#foreign_key' do 6 | subject(:relation) do 7 | Class.new(ROM::Relation) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::Integer 10 | attribute :group_id, ROM::Types::Integer.meta(foreign_key: true, target: :groups) 11 | end 12 | end.new([]) 13 | end 14 | 15 | it 'returns FK name for the given relation name' do 16 | expect(relation.foreign_key(ROM::Relation::Name[:groups])).to be(:group_id) 17 | end 18 | 19 | it 'returns FK name for the given relation name with a different dataset name' do 20 | expect(relation.foreign_key(ROM::Relation::Name[:user_groups, :groups])).to be(:group_id) 21 | end 22 | 23 | it 'generates a virtual FK when real attribute is not found' do 24 | expect(relation.foreign_key(ROM::Relation::Name[:companies])).to be(:company_id) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/inspect_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Memory::Relation, '#inspect' do 6 | subject(:relation) do 7 | Class.new(ROM::Relation[:memory]) do 8 | def self.name 9 | 'Users' 10 | end 11 | end.new([], name: ROM::Relation::Name[:users]) 12 | end 13 | 14 | specify do 15 | expect(relation.inspect).to eql(%(#)) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/loaded_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ROM::Relation::Loaded do 6 | include_context 'no container' 7 | include_context 'users and tasks' 8 | 9 | subject(:users) { ROM::Relation::Loaded.new(users_relation) } 10 | 11 | describe '#each' do 12 | it 'yields tuples from relation' do 13 | result = [] 14 | 15 | users.each do |tuple| 16 | result << tuple 17 | end 18 | 19 | expect(result).to match_array([ 20 | { name: 'Jane', email: 'jane@doe.org' }, 21 | { name: 'Joe', email: 'joe@doe.org' } 22 | ]) 23 | end 24 | 25 | it 'returns enumerator when block is not provided' do 26 | expect(users.each.to_a).to eql(users.collection.to_a) 27 | end 28 | end 29 | 30 | describe '#to_ary' do 31 | it 'coerces to an array' do 32 | expect(users.to_ary).to match_array([ 33 | { name: 'Jane', email: 'jane@doe.org' }, 34 | { name: 'Joe', email: 'joe@doe.org' } 35 | ]) 36 | end 37 | end 38 | 39 | describe '#pluck' do 40 | it 'returns a list of values under provided key' do 41 | expect(users.pluck(:email)).to eql(%w[joe@doe.org jane@doe.org]) 42 | end 43 | end 44 | 45 | describe '#primary_keys' do 46 | it 'returns a list of primary key values' do 47 | expect(users.source).to receive(:primary_key).and_return(:name) 48 | expect(users.primary_keys).to eql(%w[Joe Jane]) 49 | end 50 | end 51 | 52 | it_behaves_like 'a relation that returns one tuple' do 53 | let(:relation) { users } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/map_to_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ostruct' 4 | require 'rom/relation' 5 | 6 | RSpec.describe ROM::Relation, '#map_to' do 7 | subject(:relation) do 8 | ROM::Relation.new( 9 | dataset, 10 | name: ROM::Relation::Name[:users], 11 | schema: ROM::Relation.default_schema, 12 | mappers: mapper_registry 13 | ) 14 | end 15 | 16 | let(:mapper_registry) { ROM::MapperRegistry.build(mappers) } 17 | 18 | let(:dataset) do 19 | [{ id: 1, name: 'Jane' }, { id: 2, name: 'Joe' }] 20 | end 21 | 22 | context 'without custom mappers' do 23 | let(:mappers) { {} } 24 | 25 | it 'instantiates custom model when auto_struct is enabled' do 26 | expect( 27 | relation.with(auto_struct: true).map_to(OpenStruct).first 28 | ).to be_instance_of(OpenStruct) 29 | end 30 | 31 | it 'instantiates custom model when auto_struct is disabled' do 32 | expect(relation.map_to(OpenStruct).first).to be_instance_of(OpenStruct) 33 | end 34 | end 35 | 36 | context 'with custom mappers' do 37 | let(:mappers) do 38 | { name_list: -> users { users.map { |u| { name: u[:name] } } } } 39 | end 40 | 41 | it 'instantiates custom model when auto_struct is disabled' do 42 | user = relation.with(auto_struct: false).map_with(:name_list).map_to(OpenStruct).first 43 | 44 | expect(user).to be_instance_of(OpenStruct) 45 | expect(user.id).to be(nil) 46 | expect(user.name).to eql('Jane') 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#new' do 6 | subject(:relation) do 7 | Class.new(ROM::Relation) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::String, read: ROM::Types::Coercible::Integer 10 | attribute :name, ROM::Types::String 11 | end 12 | end.new([], **options) 13 | end 14 | 15 | let(:options) { {} } 16 | 17 | it 'returns a new relation with a new dataset' do 18 | ds = [] 19 | 20 | expect(relation.new(ds).dataset).to be(ds) 21 | end 22 | 23 | it 'returns a new relation with a new dataset and new options' do 24 | ds = [] 25 | new_rel = relation.new(ds, name: :new_name) 26 | 27 | expect(new_rel.dataset).to be(ds) 28 | expect(new_rel.name).to be(:new_name) 29 | end 30 | 31 | it 'returns a new relation with a re-stablished input/output schemas' do 32 | ds = [] 33 | new_rel = relation.new(ds, schema: relation.schema.project(:id)) 34 | 35 | expect(new_rel.dataset).to be(ds) 36 | 37 | expect(new_rel.input_schema).not_to be(relation.input_schema) 38 | expect(new_rel.output_schema).not_to be(relation.output_schema) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/output_schema_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/memory' 4 | 5 | RSpec.describe ROM::Relation, '#output_schema' do 6 | subject(:relation) do 7 | Class.new(ROM::Relation[:memory]) do 8 | schema do 9 | attribute :id, ROM::Types::String, read: ROM::Types::Integer 10 | attribute :name, ROM::Types::String 11 | end 12 | end.new(ROM::Memory::Dataset.new([])) 13 | end 14 | 15 | let(:schema) do 16 | relation.schema 17 | end 18 | 19 | it 'returns output_schema based on canonical schema' do 20 | expect(relation.output_schema).to eql( 21 | ROM::Schema::HASH_SCHEMA.schema( 22 | id: schema[:id].to_read_type, name: schema[:name].type 23 | ) 24 | ) 25 | end 26 | 27 | it 'returns output_schema based on projected schema' do 28 | projected = relation.project(schema[:id].aliased(:user_id)) 29 | 30 | expect(projected.output_schema) 31 | .to eql(ROM::Schema::HASH_SCHEMA.schema(user_id: projected.schema[:id].to_read_type)) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/preload_assoc_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#preload_assoc' do 6 | subject(:users) do 7 | ROM::Relation.new([], name: ROM::Relation::Name[:users]) 8 | end 9 | 10 | let(:tasks) do 11 | ROM::Relation.new([], name: ROM::Relation::Name[:tasks]) 12 | end 13 | 14 | let(:assoc) do 15 | double(:assoc) 16 | end 17 | 18 | it 'is auto-curried' do 19 | expect(users.preload_assoc(assoc)).to be_curried 20 | end 21 | 22 | it 'returns preloaded relation by association' do 23 | expect(assoc).to receive(:preload).with(users, tasks).and_return([]) 24 | 25 | expect(users.preload_assoc(assoc, tasks)).to eql([]) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/wrap/combine_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation/wrap' 4 | 5 | RSpec.describe ROM::Relation::Wrap, '#combine' do 6 | subject(:relation) do 7 | ROM::Relation::Wrap.new(users, [tasks]) 8 | end 9 | 10 | let(:users) do 11 | ROM::Relation.new([]) 12 | end 13 | 14 | let(:tasks) do 15 | ROM::Relation.new([]) 16 | end 17 | 18 | let(:posts) do 19 | ROM::Relation.new([]) 20 | end 21 | 22 | it 'returns a new wrap with new root as a graph relation' do 23 | posts_assoc = double(:posts_assoc, override?: false) 24 | allow(users.schema).to receive(:associations).and_return(posts: posts_assoc) 25 | 26 | expect(posts_assoc).to receive(:prepare).and_return(posts) 27 | expect(posts_assoc).to receive(:node).and_return(posts) 28 | 29 | new_graph = relation.combine(:posts) 30 | 31 | expect(new_graph.root).to be_graph 32 | expect(new_graph.nodes).to eql(relation.nodes) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /core/spec/unit/rom/relation/wrap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/relation' 4 | 5 | RSpec.describe ROM::Relation, '#wrap' do 6 | let(:users) do 7 | ROM::Relation.new([], name: ROM::Relation::Name[:users]) 8 | end 9 | 10 | let(:tasks) do 11 | ROM::Relation.new([], name: ROM::Relation::Name[:tasks]) 12 | end 13 | 14 | let(:users_assocs_set) do 15 | { tasks: tasks_assoc } 16 | end 17 | 18 | let(:tasks_assoc) do 19 | double(:tasks_assoc) 20 | end 21 | 22 | before do 23 | allow(users.schema).to receive(:associations).and_return(users_assocs_set) 24 | end 25 | 26 | context 'with a list of assoc names' do 27 | it 'returns a wrap relation' do 28 | tasks_node = double(:tasks_node) 29 | 30 | expect(tasks_assoc).to receive(:wrap).and_return(tasks_node) 31 | 32 | relation = users.wrap(:tasks) 33 | 34 | expect(relation.root).to be(users) 35 | expect(relation.nodes).to eql([tasks_node]) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/append_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#append' do 6 | subject(:schema) { left.append(*right) } 7 | 8 | let(:left) do 9 | define_schema(:users, id: :Integer, name: :String) 10 | end 11 | 12 | let(:right) do 13 | define_schema(:tasks, user_id: :Integer) 14 | end 15 | 16 | it 'returns a new schema with attributes from two schemas' do 17 | expect(schema.map(&:name)).to eql(%i[id name user_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/associations_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema/associations_dsl' 4 | 5 | RSpec.describe ROM::Schema::AssociationsDSL do 6 | subject(:dsl) do 7 | ROM::Schema::AssociationsDSL.new(ROM::Relation::Name[:users]) do 8 | has_many :posts 9 | end 10 | end 11 | 12 | describe '#call' do 13 | it 'returns a configured association set' do 14 | association_set = dsl.call 15 | 16 | expect(association_set.type).to eql('ROM::AssociationSet[:users]') 17 | expect(association_set.key?(:posts)).to be(true) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/canonical_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#canonical' do 6 | subject(:schema) { 7 | define_schema(:users, id: :Integer, name: :String) 8 | } 9 | 10 | it 'returns self by default' do 11 | expect(schema.canonical).to be(schema) 12 | end 13 | 14 | it 'returns canonical schema from a projected schema' do 15 | expect(schema.project(:id).canonical).to be(schema) 16 | end 17 | 18 | it 'is canonical' do 19 | expect(schema).to be_canonical 20 | end 21 | 22 | it 'is not canonical when projected' do 23 | expect(schema.project(:id)).to_not be_canonical 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/exclude_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#exclude' do 6 | subject(:schema) do 7 | define_schema(:users, id: :Integer, name: :String, email: :String) 8 | end 9 | 10 | let(:excluded) do 11 | schema.exclude(:id, :name) 12 | end 13 | 14 | it 'returns projected schema with renamed attributes using provided prefix' do 15 | expect(excluded.map(&:name)).to eql(%i[email]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/key_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#key?' do 6 | subject(:schema) do 7 | define_schema(:users, name: :String) 8 | end 9 | 10 | it 'returns true when an attribute exists' do 11 | expect(schema.key?(:name)).to be(true) 12 | end 13 | 14 | it 'returns false when an attribute does not exist' do 15 | expect(schema.key?(:foo)).to be(false) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/merge_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#merge' do 6 | subject(:schema) { left.merge(right) } 7 | 8 | let(:left) do 9 | define_schema(:users, id: :Integer, name: :String) 10 | end 11 | 12 | let(:right) do 13 | define_schema(:tasks, user_id: :Integer) 14 | end 15 | 16 | it 'returns a new schema with attributes from two schemas' do 17 | expect(schema.map(&:name)).to eql(%i[id name user_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/prefix_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#prefix' do 6 | subject(:schema) do 7 | define_schema(:users, id: :Integer, name: :String) 8 | end 9 | 10 | let(:prefixed) do 11 | schema.prefix(:user) 12 | end 13 | 14 | it 'returns projected schema with renamed attributes using provided prefix' do 15 | expect(prefixed.map(&:alias)).to eql(%i[user_id user_name]) 16 | expect(prefixed.map(&:name)).to eql(%i[id name]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/project_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#project' do 6 | subject(:schema) do 7 | define_schema(:users, id: :Integer, name: :String, age: :Integer) 8 | end 9 | 10 | it 'projects provided attribute names' do 11 | expect(schema.project(:name, :age).map(&:name)).to eql(%i[name age]) 12 | end 13 | 14 | it 'projects provided attributes' do 15 | expect(schema.project(schema[:name], schema[:age]).map(&:name)).to eql(%i[name age]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/rename_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#rename' do 6 | subject(:schema) do 7 | define_schema( 8 | :users, 9 | user_id: :Integer, user_name: :String, user_email: :String 10 | ) 11 | end 12 | 13 | let(:renamed) do 14 | schema.rename(user_id: :id, user_name: :name) 15 | end 16 | 17 | it 'returns projected schema with renamed attributes' do 18 | expect(renamed.map(&:name)).to eql(%i[user_id user_name user_email]) 19 | expect(renamed.map(&:alias)).to eql([:id, :name, nil]) 20 | expect(renamed.all?(&:aliased?)).to be(false) 21 | expect(renamed[:user_id]).to be_aliased 22 | expect(renamed[:user_name]).to be_aliased 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/uniq_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#uniq' do 6 | subject(:schema) { left.merge(right) } 7 | 8 | let(:left) do 9 | define_schema(:users, id: :Integer, name: :String) 10 | end 11 | 12 | let(:right) do 13 | define_schema(:tasks, id: :Integer, user_id: :Integer) 14 | end 15 | 16 | it 'returns a new schema with unique attributes from two schemas' do 17 | expect(schema.uniq.map(&:name)).to eql(%i[id name user_id]) 18 | end 19 | 20 | it 'accepts a block' do 21 | expect(schema.uniq(&:primitive).map(&:name)).to eql(%i[id name]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /core/spec/unit/rom/schema/wrap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/schema' 4 | 5 | RSpec.describe ROM::Schema, '#wrap' do 6 | subject(:schema) do 7 | define_schema(:users, id: :Integer, name: :String) 8 | end 9 | 10 | let(:wrapped) do 11 | schema.wrap(:users) 12 | end 13 | 14 | it 'returns projected schema with renamed attributes using provided prefix' do 15 | expect(wrapped.map(&:alias)).to eql(%i[users_id users_name]) 16 | expect(wrapped.map(&:name)).to eql(%i[id name]) 17 | expect(wrapped.all?(&:wrapped?)).to be(true) 18 | expect(wrapped.wrap(:foo)).to eql(wrapped) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: postgres:latest 5 | ports: 6 | - 5432:5432 7 | environment: 8 | POSTGRES_DB: rom_repository 9 | 10 | app: 11 | build: . 12 | command: ./docker_start.sh 13 | environment: 14 | BASE_DB_URI: postgres@db/rom_repository 15 | volumes: 16 | - .:/opt/rom 17 | - bundle_box:/opt/bundle_box 18 | links: 19 | - db 20 | 21 | volumes: 22 | bundle_box: 23 | 24 | -------------------------------------------------------------------------------- /docker_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | bundle check || bundle install 3 | tail -f Gemfile 4 | -------------------------------------------------------------------------------- /lib/rom.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom-core' 4 | require 'rom-repository' 5 | require 'rom-changeset' 6 | -------------------------------------------------------------------------------- /lib/rom/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | VERSION = '5.4.2' 5 | end 6 | -------------------------------------------------------------------------------- /repository/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | --require ./spec/spec_helper 4 | -------------------------------------------------------------------------------- /repository/.simplecov: -------------------------------------------------------------------------------- 1 | ../.simplecov -------------------------------------------------------------------------------- /repository/.yardopts: -------------------------------------------------------------------------------- 1 | --query '@api.text != "private"' 2 | --embed-mixins 3 | -o ../doc/rom-repository 4 | -r README.md 5 | --markup-provider=redcarpet 6 | --markup=markdown 7 | --files CHANGELOG.md 8 | -------------------------------------------------------------------------------- /repository/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | COMPONENTS = %w[core changeset].freeze 4 | 5 | eval_gemfile '../Gemfile' 6 | -------------------------------------------------------------------------------- /repository/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 Piotr Solnica 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /repository/README.md: -------------------------------------------------------------------------------- 1 | [gem]: https://rubygems.org/gems/rom-repository 2 | 3 | # rom-repository 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rom-repository.svg)][gem] 6 | 7 | Repositories for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping, changesets and commands. 8 | 9 | Resources: 10 | 11 | * [User documentation](http://rom-rb.org/learn/repositories) 12 | * [API documentation](http://api.rom-rb.org/rom/) 13 | 14 | ## License 15 | 16 | See `LICENSE` file. 17 | -------------------------------------------------------------------------------- /repository/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | task default: [:ci] 8 | 9 | desc 'Run CI tasks' 10 | task ci: [:spec] 11 | 12 | begin 13 | require 'rubocop/rake_task' 14 | 15 | Rake::Task[:default].enhance [:rubocop] 16 | 17 | RuboCop::RakeTask.new do |task| 18 | task.options << '--display-cop-names' 19 | end 20 | rescue LoadError 21 | # ignore 22 | end 23 | -------------------------------------------------------------------------------- /repository/benchmarks/command_vs_changeset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'setup' 4 | 5 | benchmark('create command vs changeset') do |x| 6 | x.report('command') do 7 | users.command(:create).call(name: 'Jane', email: 'jane@doe.org', age: 21) 8 | end 9 | 10 | x.report('changeset') do 11 | users.changeset(:create, name: 'Jane', email: 'jane@doe.org', age: 21).commit 12 | end 13 | 14 | x.compare! 15 | end 16 | 17 | benchmark('update command vs changeset') do |x| 18 | x.prepare do 19 | users.command(:create).call(name: 'Jane', email: 'jane@doe.org', age: 21) 20 | users.command(:create).call(name: 'John', email: 'john@doe.org', age: 21) 21 | end 22 | 23 | x.report('command') do 24 | users.where(name: 'Jane').command(:update).call(name: 'Jane Doe') 25 | end 26 | 27 | x.report('changeset') do 28 | users.where(name: 'Jane').changeset(:update, name: 'Jane Doe').commit 29 | end 30 | 31 | x.compare! 32 | end 33 | 34 | benchmark('delete command vs changeset') do |x| 35 | x.report('command') do 36 | users.command(:create).call(name: 'Jane', email: 'jane@doe.org', age: 21) 37 | end 38 | 39 | x.report('changeset') do 40 | users.changeset(:create, name: 'Jane', email: 'jane@doe.org', age: 21).commit 41 | end 42 | 43 | x.compare! 44 | end 45 | -------------------------------------------------------------------------------- /repository/benchmarks/gc_suite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Enable and start GC before each job run. Disable GC afterwards. 4 | # 5 | # Inspired by https://www.omniref.com/ruby/2.2.1/symbols/Benchmark/bm?#annotation=4095926&line=182 6 | class GCSuite 7 | def warming(*) 8 | run_gc 9 | end 10 | 11 | def running(*) 12 | run_gc 13 | end 14 | 15 | def warmup_stats(*); end 16 | 17 | def add_report(*); end 18 | 19 | private 20 | 21 | def run_gc 22 | GC.enable 23 | GC.start 24 | GC.disable 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /repository/benchmarks/profile_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'setup' 4 | require_relative 'seed' 5 | require 'hotch' 6 | 7 | Hotch() do 8 | COUNT.times do |i| 9 | user_repo[i + 1] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /repository/benchmarks/seed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | USER_SEED = COUNT.times.map { |i| 4 | { id: i + 1, 5 | name: "User #{i + 1}", 6 | email: "email_#{i}@domain.com", 7 | age: i * 10 } 8 | } 9 | 10 | TASK_SEED = USER_SEED.map { |user| 11 | 3.times.map do |i| 12 | { user_id: user[:id], title: "Task #{i + 1}" } 13 | end 14 | }.flatten 15 | 16 | def seed 17 | hr 18 | 19 | puts "SEEDING #{USER_SEED.count} users" 20 | USER_SEED.each do |attributes| 21 | rom.relations.users.insert(attributes) 22 | end 23 | 24 | puts "SEEDING #{TASK_SEED.count} tasks" 25 | TASK_SEED.each do |attributes| 26 | rom.relations.tasks.insert(attributes) 27 | end 28 | 29 | hr 30 | end 31 | 32 | seed 33 | 34 | hr 35 | puts "INSERTED #{rom.relations.users.count} users via ROM/Sequel" 36 | puts "INSERTED #{rom.relations.tasks.count} tasks via ROM/Sequel" 37 | hr 38 | -------------------------------------------------------------------------------- /repository/docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: 3 3 | chapter: Repositories 4 | sections: 5 | - quick-start 6 | - reading-simple-objects 7 | - reading-aggregates 8 | - writing-aggregates 9 | --- 10 | 11 | In this section you can learn how to work with ROM repositories. 12 | 13 | * [Quick Start](/learn/repository/%{version}/quick-start) 14 | * [Reading Simple Objects](/learn/repository/%{version}/reading-simple-objects) 15 | * [Reading Aggregates](/learn/repository/%{version}/reading-aggregates) 16 | * [Writing Aggregates](/learn/repository/%{version}/writing-aggregates) 17 | -------------------------------------------------------------------------------- /repository/examples/sql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom-repository' 4 | 5 | conf = ROM::Configuration.new(:sql, 'sqlite::memory') 6 | 7 | migration = conf.gateways[:default].migration do 8 | change do 9 | create_table(:users) do 10 | primary_key :id 11 | column :name, String, null: false 12 | column :email, String, null: false 13 | end 14 | end 15 | end 16 | 17 | migration.apply(conf.gateways[:default].connection, :up) 18 | 19 | class Users < ROM::Relation[:sql] 20 | schema(infer: true) 21 | 22 | def by_id(id) 23 | where(id: id) 24 | end 25 | end 26 | 27 | conf.register_relation(Users) 28 | rom = ROM.container(conf) 29 | 30 | class UserRepo < ROM::Repository[:users] 31 | commands :create, update: :by_id, delete: :by_id 32 | 33 | def [](id) 34 | users.by_id(id).one! 35 | end 36 | 37 | def all 38 | users.to_a 39 | end 40 | end 41 | 42 | user_repo = UserRepo.new(rom) 43 | 44 | user = user_repo.create(name: 'Jane', email: 'jane@doe.org') 45 | 46 | puts user.inspect 47 | 48 | user_repo.update(user.id, name: 'Jane Doe') 49 | 50 | puts user_repo[user.id].inspect 51 | 52 | user_repo.delete(user.id) 53 | 54 | puts user_repo.all.inspect 55 | -------------------------------------------------------------------------------- /repository/lib/rom-repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/core' 4 | require 'rom/repository' 5 | -------------------------------------------------------------------------------- /repository/lib/rom/repository/session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | # TODO: finish this in 1.1.0 5 | # 6 | # @api private 7 | class Session 8 | include Dry::Equalizer(:queue, :status) 9 | 10 | attr_reader :repo 11 | 12 | attr_reader :queue 13 | 14 | attr_reader :status 15 | 16 | def initialize(repo) 17 | @repo = repo 18 | @status = :pending 19 | initialize_queue! 20 | end 21 | 22 | def add(changeset) 23 | queue << changeset 24 | self 25 | end 26 | 27 | def commit! 28 | queue.each(&:commit) 29 | 30 | @status = :success 31 | 32 | self 33 | rescue StandardError => e 34 | @status = :failure 35 | raise e 36 | ensure 37 | initialize_queue! 38 | end 39 | 40 | def pending? 41 | status.equal?(:pending) 42 | end 43 | 44 | def success? 45 | status.equal?(:success) 46 | end 47 | 48 | def failure? 49 | status.equal?(:failure) 50 | end 51 | 52 | private 53 | 54 | def initialize_queue! 55 | @queue = [] 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /repository/lib/rom/repository/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | class Repository 5 | VERSION = '5.4.2' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /repository/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rom-rb/rom/d2de00f6249d17aea7965573972633677018f4cf/repository/log/.gitkeep -------------------------------------------------------------------------------- /repository/rom-repository.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/rom/repository/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'rom-repository' 7 | gem.summary = 'Repository abstraction for rom-rb' 8 | gem.description = gem.summary 9 | gem.author = 'Piotr Solnica' 10 | gem.email = 'piotr.solnica+oss@gmail.com' 11 | gem.homepage = 'http://rom-rb.org' 12 | gem.require_paths = ['lib'] 13 | gem.version = ROM::Repository::VERSION.dup 14 | gem.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'lib/**/*'] 15 | gem.license = 'MIT' 16 | gem.metadata = { 17 | 'source_code_uri' => 'https://github.com/rom-rb/rom/tree/master/repository', 18 | 'documentation_uri' => 'https://api.rom-rb.org/rom/', 19 | 'mailing_list_uri' => 'https://discourse.rom-rb.org/', 20 | 'bug_tracker_uri' => 'https://github.com/rom-rb/rom/issues', 21 | 'rubygems_mfa_required' => 'true' 22 | } 23 | 24 | gem.add_dependency 'dry-core', '~> 1.0' 25 | gem.add_dependency 'dry-initializer', '~> 3.2' 26 | gem.add_dependency 'rom-core', '~> 5.4' 27 | end 28 | -------------------------------------------------------------------------------- /repository/spec/integration/plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'repository plugin' do 4 | include_context 'database' 5 | include_context 'relations' 6 | include_context 'seeds' 7 | include_context 'repo' 8 | 9 | let(:nullify_plugin) do 10 | Module.new do 11 | def self.apply(target, **) 12 | target.prepend(self) 13 | end 14 | 15 | def prepare_relation(*) 16 | super.where { `1 = 0` } 17 | end 18 | end 19 | end 20 | 21 | before do 22 | plugin = nullify_plugin 23 | 24 | ROM.plugins do 25 | register :nullify_datasets, plugin, type: :repository 26 | end 27 | end 28 | 29 | let(:user_repo) do 30 | Class.new(repo_class) { use :nullify_datasets }.new(rom) 31 | end 32 | 33 | it 'always returns empty result set' do 34 | expect(user_repo.all_users.to_a).to eql([]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /repository/spec/integration/root_repository_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Repository::Root do 4 | subject(:repo) do 5 | klass.new(rom) 6 | end 7 | 8 | let(:klass) do 9 | Class.new(ROM::Repository[:users]) 10 | end 11 | 12 | include_context 'database' 13 | include_context 'relations' 14 | 15 | describe '.[]' do 16 | it 'creates a pre-configured root repo class' do 17 | klass = ROM::Repository[:users] 18 | 19 | expect(klass.root).to be(:users) 20 | 21 | child = klass[:users] 22 | 23 | expect(child.root).to be(:users) 24 | expect(child < klass).to be(true) 25 | end 26 | end 27 | 28 | describe 'inheritance' do 29 | it 'inherits root and relations' do 30 | klass = Class.new(repo.class) 31 | 32 | expect(klass.root).to be(:users) 33 | end 34 | 35 | it 'creates base root class' do 36 | klass = Class.new(ROM::Repository)[:users] 37 | 38 | expect(klass.root).to be(:users) 39 | end 40 | end 41 | 42 | describe 'overriding reader' do 43 | it 'works with super' do 44 | klass.class_eval do 45 | def users 46 | super.limit(10) 47 | end 48 | end 49 | 50 | expect(repo.users.dataset.opts[:limit]).to be(10) 51 | end 52 | end 53 | 54 | describe '#root' do 55 | it 'returns configured root relation' do 56 | expect(repo.root.dataset).to be(rom.relations[:users].dataset) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /repository/spec/shared/mappers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'mappers' do 4 | let(:user_mappers) { users.mappers[:user] } 5 | let(:task_mappers) { tasks.mappers[:task] } 6 | let(:tag_mappers) { tags.mappers[:tag] } 7 | 8 | before do 9 | configuration.mappers do 10 | define(:users) do 11 | model Test::Models::User 12 | register_as :user 13 | 14 | attribute :id 15 | attribute :name 16 | end 17 | 18 | define(:tasks) do 19 | model Test::Models::Task 20 | register_as :task 21 | 22 | attribute :id 23 | attribute :user_id 24 | attribute :title 25 | end 26 | 27 | define(:tags) do 28 | model Test::Models::Tag 29 | register_as :tag 30 | 31 | attribute :id 32 | attribute :task_id 33 | attribute :name 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /repository/spec/shared/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'models' do 4 | let(:user_model) { Test::Models::User } 5 | let(:task_model) { Test::Models::Task } 6 | let(:tag_model) { Test::Models::Tag } 7 | 8 | before do 9 | module Test 10 | module Models 11 | class User 12 | include Dry::Equalizer(:id, :name) 13 | 14 | attr_reader :id 15 | 16 | attr_reader :name 17 | 18 | def initialize(attrs) 19 | @id = attrs[:id] 20 | @name = attrs[:name] 21 | end 22 | end 23 | 24 | class Task 25 | include Dry::Equalizer(:id, :user_id, :title) 26 | 27 | attr_reader :id 28 | 29 | attr_reader :user_id 30 | 31 | attr_reader :title 32 | 33 | def initialize(attrs) 34 | @id = attrs[:id] 35 | @name = attrs[:name] 36 | @title = attrs[:title] 37 | end 38 | end 39 | 40 | class Tag 41 | include Dry::Equalizer(:id, :task_id, :name) 42 | 43 | attr_reader :id 44 | 45 | attr_reader :task_id 46 | 47 | attr_reader :name 48 | 49 | def initialize(attrs) 50 | @id = attrs[:id] 51 | @task_id = attrs[:task_id] 52 | @name = attrs[:name] 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /repository/spec/shared/repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context('repo') do 4 | include_context 'models' 5 | include_context 'mappers' 6 | 7 | let(:repo) { repo_class.new(rom) } 8 | 9 | let(:repo_class) do 10 | Class.new(ROM::Repository[:users]) do 11 | def find_users(criteria) 12 | users.find(criteria) 13 | end 14 | 15 | def all_users 16 | users.all 17 | end 18 | 19 | def all_users_as_users 20 | users.map_with(:user).all 21 | end 22 | 23 | def users_with_posts_and_their_labels 24 | users.combine(posts: [:labels]) 25 | end 26 | 27 | def posts_with_labels 28 | posts.combine(:labels) 29 | end 30 | 31 | def label_with_posts 32 | labels.combine(:post) 33 | end 34 | 35 | def task_with_owner 36 | tasks.find(id: 2).combine(:owner) 37 | end 38 | 39 | def task_with_user 40 | tasks.find(id: 2).combine(:user) 41 | end 42 | 43 | def tasks_with_tags(tasks = self.tasks) 44 | tasks.combine(:tags) 45 | end 46 | 47 | def tag_with_wrapped_task 48 | tags.wrap(:task) 49 | end 50 | end 51 | end 52 | 53 | let(:comments_repo) do 54 | Class.new(ROM::Repository[:comments]) do 55 | def comments_with_likes 56 | comments.combine(:likes) 57 | end 58 | 59 | def comments_with_emotions 60 | root.combine(:emotions) 61 | end 62 | end.new(rom) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /repository/spec/shared/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'seeds' do 4 | before do 5 | jane_id = conn[:users].insert name: 'Jane' 6 | joe_id = conn[:users].insert name: 'Joe' 7 | 8 | conn[:tasks].insert user_id: joe_id, title: 'Joe Task' 9 | task_id = conn[:tasks].insert user_id: jane_id, title: 'Jane Task' 10 | 11 | conn[:tags].insert task_id: task_id, name: 'red' 12 | 13 | jane_post_id = conn[:posts].insert( 14 | author_id: jane_id, 15 | title: 'Hello From Jane', 16 | body: 'Jane Post' 17 | ) 18 | joe_post_id = conn[:posts].insert( 19 | author_id: joe_id, 20 | title: 'Hello From Joe', 21 | body: 'Joe Post' 22 | ) 23 | 24 | red_id = conn[:labels].insert name: 'red' 25 | green_id = conn[:labels].insert name: 'green' 26 | blue_id = conn[:labels].insert name: 'blue' 27 | 28 | conn[:posts_labels].insert post_id: jane_post_id, label_id: red_id 29 | conn[:posts_labels].insert post_id: jane_post_id, label_id: blue_id 30 | 31 | conn[:posts_labels].insert post_id: joe_post_id, label_id: green_id 32 | 33 | conn[:messages].insert author: 'Jane', body: 'Hello folks' 34 | conn[:messages].insert author: 'Joe', body: 'Hello Jane' 35 | 36 | conn[:reactions].insert message_id: 1, author: 'Joe' 37 | conn[:reactions].insert message_id: 1, author: 'Anonymous' 38 | conn[:reactions].insert message_id: 2, author: 'Jane' 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /repository/spec/support/mapper_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MapperRegistry 4 | def mapper_for(relation) 5 | relation.mapper 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /repository/spec/unit/repository/class_interface/root_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom/repository' 4 | 5 | RSpec.describe ROM::Repository, '.[]' do 6 | subject(:repo) do 7 | Class.new(ROM::Repository) 8 | end 9 | 10 | let(:relation) { :users } 11 | 12 | it 'creates a preconfigured ROM::Repository:Root class' do 13 | expect(repo[relation].root).to be(relation) 14 | end 15 | 16 | it 'caches the class' do 17 | expect(repo[relation]).to be(repo[relation]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /repository/spec/unit/repository/inspect_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Repository, '#inspect' do 4 | subject(:repo) do 5 | Class.new(ROM::Repository) do 6 | def self.to_s 7 | 'UserRepo' 8 | end 9 | end.new(rom) 10 | end 11 | 12 | include_context 'database' 13 | include_context 'relations' 14 | 15 | specify do 16 | expect(repo.inspect).to eql('#') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /repository/spec/unit/session_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Session do 4 | subject(:session) do 5 | ROM::Session.new(repo) 6 | end 7 | 8 | let(:repo) { instance_double(ROM::Repository) } 9 | let(:create_changeset) { instance_double(ROM::Changeset::Create, relation: relation) } 10 | let(:delete_changeset) { instance_double(ROM::Changeset::Delete, relation: relation) } 11 | let(:relation) { double.as_null_object } 12 | 13 | describe '#pending?' do 14 | it 'returns true before commit' do 15 | expect(session).to be_pending 16 | end 17 | 18 | it 'returns false after commit' do 19 | expect(session.commit!).to_not be_pending 20 | end 21 | end 22 | 23 | describe '#commit!' do 24 | it 'executes ops and restores pristine state' do 25 | expect(create_changeset).to receive(:commit).and_return(true) 26 | 27 | session.add(create_changeset).commit! 28 | session.commit! 29 | 30 | expect(session).to be_success 31 | end 32 | 33 | it 'executes ops and restores pristine state when exception was raised' do 34 | expect(create_changeset).to_not receive(:commit) 35 | expect(delete_changeset).to receive(:commit).and_raise(StandardError, 'oops') 36 | 37 | expect { 38 | session.add(delete_changeset) 39 | session.add(create_changeset) 40 | session.commit! 41 | }.to raise_error(StandardError, 'oops') 42 | 43 | expect(session).to be_failure 44 | 45 | session.commit! 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /rom.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/rom/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'rom' 7 | gem.summary = 'Persistence and mapping toolkit for Ruby' 8 | gem.description = gem.summary 9 | gem.author = 'Piotr Solnica' 10 | gem.email = 'piotr.solnica+oss@gmail.com' 11 | gem.homepage = 'http://rom-rb.org' 12 | gem.version = ROM::VERSION.dup 13 | gem.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'lib/**/*'] 14 | gem.license = 'MIT' 15 | gem.metadata = { 16 | 'source_code_uri' => 'https://github.com/rom-rb/rom', 17 | 'documentation_uri' => 'https://api.rom-rb.org/rom/', 18 | 'mailing_list_uri' => 'https://discourse.rom-rb.org/', 19 | 'bug_tracker_uri' => 'https://github.com/rom-rb/rom/issues', 20 | 'rubygems_mfa_required' => 'true' 21 | } 22 | 23 | gem.add_dependency 'rom-changeset', '~> 5.4' 24 | gem.add_dependency 'rom-core', '~> 5.4' 25 | gem.add_dependency 'rom-repository', '~> 5.4', '>= 5.4.2' 26 | end 27 | -------------------------------------------------------------------------------- /spec/integration/gateways/gateway_params_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Gateways / keyword arguments' do 4 | before do 5 | @adapter = Module.new 6 | class @adapter::Gateway 7 | extend ROM::Initializer 8 | 9 | param :param 10 | option :option 11 | end 12 | ROM.register_adapter(:my_adapter, @adapter) 13 | end 14 | 15 | specify do 16 | gw = @adapter::Gateway.new(:param, option: :option) 17 | expect(gw.param).to eq :param 18 | expect(gw.option).to eq :option 19 | end 20 | 21 | specify do 22 | conf = ROM::Configuration.new(:my_adapter, :param, option: :option) 23 | container = ROM.container(conf) 24 | gw = container.gateways[:default] 25 | expect(gw.param).to eq :param 26 | expect(gw.option).to eq :option 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tempfile' 4 | 5 | SPEC_ROOT = root = Pathname(__FILE__).dirname 6 | 7 | if ENV['COVERAGE'] == 'true' 8 | require 'codacy-coverage' 9 | Codacy::Reporter.start(partial: true) 10 | end 11 | 12 | require 'rom/core' 13 | 14 | Dir[root.join('support/*.rb').to_s].each do |f| 15 | require f 16 | end 17 | 18 | Dir[root.join('shared/*.rb').to_s].each do |f| 19 | require f 20 | end 21 | 22 | # Namespace holding all objects created during specs 23 | module Test 24 | def self.remove_constants 25 | constants.each(&method(:remove_const)) 26 | end 27 | end 28 | 29 | RSpec.configure do |config| 30 | config.after do 31 | Test.remove_constants 32 | end 33 | 34 | config.disable_monkey_patching! 35 | config.filter_run_when_matching :focus 36 | config.warnings = true 37 | end 38 | --------------------------------------------------------------------------------