├── .devtools └── templates │ ├── changelog.erb │ └── release.erb ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ----please-don-t-ask-for-support-via-issues.md │ ├── ---bug-report.md │ ├── ---feature-request.md │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── docsite.yml │ ├── rubocop.yml │ └── sync_configs.yml ├── .gitignore ├── .repobot.yml ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── changelog.yml ├── docsite └── source │ └── index.html.md ├── examples ├── github.rb └── repository_with_combine.rb ├── lib ├── rom-http.rb └── rom │ ├── http.rb │ └── http │ ├── associations.rb │ ├── associations │ ├── many_to_many.rb │ ├── many_to_one.rb │ ├── one_to_many.rb │ └── one_to_one.rb │ ├── attribute.rb │ ├── commands.rb │ ├── commands │ ├── create.rb │ ├── delete.rb │ └── update.rb │ ├── dataset.rb │ ├── error.rb │ ├── gateway.rb │ ├── handlers.rb │ ├── handlers │ └── json.rb │ ├── mapper_compiler.rb │ ├── relation.rb │ ├── schema.rb │ ├── schema │ └── dsl.rb │ ├── transformer.rb │ ├── types.rb │ └── version.rb ├── project.yml ├── rom-http.gemspec └── spec ├── fixtures └── vcr_cassettes │ ├── github_repos.yml │ ├── posts_and_users.yml │ ├── posts_with_user.yml │ └── user_with_posts.yml ├── integration └── abstract │ ├── associations_spec.rb │ ├── commands │ ├── create_spec.rb │ ├── delete_spec.rb │ └── update_spec.rb │ └── relation_spec.rb ├── shared ├── setup.rb └── users_and_tasks.rb ├── spec_helper.rb ├── support ├── coverage.rb ├── mutant.rb ├── rspec_options.rb └── warnings.rb └── unit └── rom └── http ├── dataset_spec.rb ├── gateway_spec.rb ├── handlers └── json_spec.rb └── relation_spec.rb /.devtools/templates/changelog.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% releases.each_with_index do |r, idx| %> 5 | ## <%= r.version %> <%= r.date %> 6 | 7 | <% if r.summary %> 8 | <%= r.summary %> 9 | 10 | <% end %> 11 | 12 | <% if r.added? %> 13 | ### Added 14 | 15 | <% r.added.each do |log| %> 16 | - <%= log %> 17 | <% end %> 18 | 19 | <% end %> 20 | <% if r.fixed? %> 21 | ### Fixed 22 | 23 | <% r.fixed.each do |log| %> 24 | - <%= log %> 25 | <% end %> 26 | 27 | <% end %> 28 | <% if r.changed? %> 29 | ### Changed 30 | 31 | <% r.changed.each do |log| %> 32 | - <%= log %> 33 | <% end %> 34 | <% end %> 35 | <% curr_ver = r.date ? "v#{r.version}" : 'main' %> 36 | <% prev_rel = releases[idx + 1] %> 37 | <% if prev_rel %> 38 | <% ver_range = "v#{prev_rel.version}...#{curr_ver}" %> 39 | 40 | [Compare <%=ver_range%>](https://github.com/rom-rb/<%= project.name %>/compare/<%=ver_range%>) 41 | <% end %> 42 | 43 | <% end %> 44 | -------------------------------------------------------------------------------- /.devtools/templates/release.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if latest_release.summary %> 5 | <%= latest_release.summary %> 6 | 7 | <% end %> 8 | 9 | <% if latest_release.added? %> 10 | ### Added 11 | 12 | <% latest_release.added.each do |log| %> 13 | - <%= log %> 14 | <% end %> 15 | 16 | <% end %> 17 | <% if latest_release.fixed? %> 18 | ### Fixed 19 | 20 | <% latest_release.fixed.each do |log| %> 21 | - <%= log %> 22 | <% end %> 23 | 24 | <% end %> 25 | <% if latest_release.changed? %> 26 | ### Changed 27 | 28 | <% latest_release.changed.each do |log| %> 29 | - <%= log %> 30 | <% end %> 31 | <% end %> 32 | <% if previous_release %> 33 | <% ver_range = "v#{previous_release.version}...v#{latest_release.version}" %> 34 | 35 | [Compare <%=ver_range%>](https://github.com/rom-rb/<%= project.name %>/compare/<%=ver_range%>) 36 | <% end %> 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⚠️ Please don't ask for support via issues" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Before you submit this: WE ONLY ACCEPT BUG REPORTS AND FEATURE REQUESTS** 11 | 12 | For more information see [our contribution guidelines](https://github.com/rom-rb/rom/blob/main/CONTRIBUTING.md) 13 | 14 | **Describe the bug** 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | 20 | Provide detailed steps to reproduce, an executable script would be best. 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Your environment** 27 | 28 | - Affects my production application: **YES/NO** 29 | - Ruby version: ... 30 | - OS: ... 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F6E0 Feature request" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | Summary of what the feature is supposed to do. 11 | 12 | ## Examples 13 | 14 | Code examples showing how the feature could be used. 15 | 16 | ## Resources 17 | 18 | Additional information, like a link to the discussion forum thread where the feature was discussed etc. 19 | -------------------------------------------------------------------------------- /.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.rom-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 rom-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.rom-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/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-rb/template-gem repo 2 | name: ci 3 | 4 | on: 5 | push: 6 | paths: 7 | - ".github/workflows/ci.yml" 8 | - "lib/**" 9 | - "*.gemspec" 10 | - "spec/**" 11 | - "Rakefile" 12 | - "Gemfile" 13 | - "Gemfile.devtools" 14 | - ".rubocop.yml" 15 | - "project.yml" 16 | pull_request: 17 | branches: 18 | - main 19 | create: 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | name: Tests 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | ruby: 29 | - "3.0" 30 | - "2.7" 31 | include: 32 | - ruby: "3.1" 33 | coverage: "true" 34 | env: 35 | COVERAGE: ${{matrix.coverage}} 36 | COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v1 40 | - name: Install package dependencies 41 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 42 | - name: Set up Ruby 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{matrix.ruby}} 46 | - name: Install latest bundler 47 | run: | 48 | gem install bundler --no-document 49 | bundle config set without 'tools benchmarks docs' 50 | - name: Bundle install 51 | run: bundle install --jobs 4 --retry 3 52 | - name: Run all tests 53 | run: bundle exec rake 54 | - name: Run codacy-coverage-reporter 55 | uses: codacy/codacy-coverage-reporter-action@master 56 | if: env.COVERAGE == 'true' && env.COVERAGE_TOKEN != '' 57 | with: 58 | project-token: ${{secrets.CODACY_PROJECT_TOKEN}} 59 | coverage-reports: coverage/coverage.xml 60 | -------------------------------------------------------------------------------- /.github/workflows/docsite.yml: -------------------------------------------------------------------------------- 1 | # this file is managed by rom-rb/devtools project 2 | 3 | name: docsite 4 | 5 | on: 6 | push: 7 | paths: 8 | - docsite/** 9 | - .github/workflows/docsite.yml 10 | branches: 11 | - main 12 | - release-** 13 | tags: 14 | 15 | jobs: 16 | update-docs: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - run: | 23 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 24 | - name: Set up Ruby 25 | uses: actions/setup-ruby@v1 26 | with: 27 | ruby-version: "2.6.x" 28 | - name: Set up git user 29 | run: | 30 | git config --local user.email "rom-bot@rom-rb.org" 31 | git config --local user.name "rom-bot" 32 | - name: Install dependencies 33 | run: gem install ossy --no-document 34 | - name: Update release branches 35 | run: | 36 | branches=`git log --format=%B -n 1 $GITHUB_SHA | grep "docsite:release-" || echo "nothing"` 37 | 38 | if [[ ! $branches -eq "nothing" ]]; then 39 | for b in $branches 40 | do 41 | name=`echo $b | ruby -e 'puts gets[/:(.+)/, 1].gsub(/\s+/, "")'` 42 | 43 | echo "merging $GITHUB_SHA to $name" 44 | 45 | git checkout -b $name --track origin/$name 46 | 47 | echo `git log -n 1` 48 | 49 | git cherry-pick $GITHUB_SHA -m 1 50 | done 51 | 52 | git push --all "https://rom-bot:${{secrets.GH_PAT}}@github.com/$GITHUB_REPOSITORY.git" 53 | 54 | git checkout main 55 | else 56 | echo "no need to update branches" 57 | fi 58 | - name: Trigger rom-rb.org deploy 59 | env: 60 | GITHUB_LOGIN: rom-bot 61 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 62 | run: ossy github workflow rom-rb/rom-rb.org ci 63 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: "RuboCop" 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "main" 7 | paths: 8 | - "**/*.rb" 9 | - "**/*.rake" 10 | - "Rakefile" 11 | - "Gemfile*" 12 | - ".rubocop.yml" 13 | pull_request: 14 | branches: 15 | - "main" 16 | - "master" 17 | types: 18 | - "opened" 19 | - "synchronize" 20 | workflow_dispatch: 21 | jobs: 22 | run: 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.type }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | type: ["Style", "Layout", "Naming", "Lint", "Metrics", "Security"] 29 | steps: 30 | - name: Clone 31 | uses: actions/checkout@v2 32 | - name: Get git diff 33 | id: get_diff 34 | uses: technote-space/get-diff-action@v4 35 | with: 36 | PATTERNS: | 37 | **/*.rb 38 | **/*.rake 39 | Gemfile 40 | Rakefile 41 | - name: Check ${{ matrix.type }} 42 | uses: action-hero/actions/rubocop@main 43 | if: ${{ env.GIT_DIFF != '' }} 44 | with: 45 | diff: ${{ env.GIT_DIFF }} 46 | type: ${{ matrix.type }} 47 | -------------------------------------------------------------------------------- /.github/workflows/sync_configs.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-rb/template-gem repo 2 | 3 | name: Changelog 4 | 5 | on: 6 | push: 7 | paths: 8 | - "changelog.yml" 9 | branches: 10 | - "master" 11 | pull_request: 12 | branches: 13 | - "main" 14 | types: [closed] 15 | 16 | jobs: 17 | sync: 18 | runs-on: ubuntu-latest 19 | if: github.event.pull_request.merged == true 20 | name: Update 21 | env: 22 | GITHUB_LOGIN: rom-bot 23 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 24 | steps: 25 | - name: Checkout ${{github.repository}} 26 | uses: actions/checkout@v1 27 | - name: Checkout devtools 28 | uses: actions/checkout@v2 29 | with: 30 | repository: rom-rb/devtools 31 | path: tmp/devtools 32 | - name: Setup git user 33 | run: | 34 | git config --local user.email "rom-bot@rom-rb.org" 35 | git config --local user.name "rom-bot" 36 | - name: Set up Ruby 37 | uses: actions/setup-ruby@v1 38 | with: 39 | ruby-version: "2.6" 40 | - name: Install dependencies 41 | run: gem install ossy --no-document 42 | - name: Update changelog.yml from commit 43 | run: tmp/devtools/bin/update-changelog-from-commit $GITHUB_SHA 44 | - name: Compile CHANGELOG.md 45 | run: tmp/devtools/bin/compile-changelog 46 | - name: Commit 47 | run: | 48 | git add -A 49 | git commit -m "Update CHANGELOG.md" || echo "nothing to commit" 50 | - name: Push changes 51 | run: | 52 | git pull --rebase origin main 53 | git push https://rom-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:main 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /.yardoc 3 | /_yardoc 4 | /coverage 5 | /vendor 6 | /doc 7 | /pkg 8 | /spec/reports 9 | /tmp 10 | Gemfile.lock 11 | *.bundle 12 | *.so 13 | *.o 14 | *.a 15 | mkmf.log 16 | log/*.log 17 | 18 | -------------------------------------------------------------------------------- /.repobot.yml: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # This is a config synced from rom-rb/template-gem repo 5 | ########################################################### 6 | 7 | sources: 8 | - repo: rom-rb/template-gem 9 | sync: 10 | - ".repobot.yml.erb" 11 | - ".devtools/templates/*.sync:${{dir}}/${{name}}" 12 | - ".github/**/*.*" 13 | - ".rspec" 14 | - ".rubocop.yml" 15 | - "spec/support/*" 16 | - "CODE_OF_CONDUCT.md" 17 | - "CONTRIBUTING.md" 18 | - "CODEOWNERS" 19 | - "LICENSE.erb" 20 | - "README.erb" 21 | - "Gemfile.devtools" 22 | - repo: repobot-app/workflows 23 | sync: 24 | - ".github/workflows/*.yml" 25 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | --warnings 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This is a config synced from rom-rb/template-gem repo 2 | AllCops: 3 | TargetRubyVersion: 2.7 4 | NewCops: disable 5 | SuggestExtensions: false 6 | Exclude: 7 | - spec/support/coverage.rb 8 | - spec/support/warnings.rb 9 | - spec/support/rspec_options.rb 10 | - Gemfile.devtools 11 | - "*.gemspec" 12 | 13 | Layout/ArgumentAlignment: 14 | Enabled: false 15 | EnforcedStyle: with_fixed_indentation 16 | 17 | Layout/SpaceAroundMethodCallOperator: 18 | Enabled: false 19 | 20 | Layout/SpaceInLambdaLiteral: 21 | Enabled: false 22 | 23 | Layout/MultilineMethodCallIndentation: 24 | Enabled: true 25 | EnforcedStyle: indented 26 | 27 | Layout/FirstArrayElementIndentation: 28 | EnforcedStyle: consistent 29 | 30 | Layout/SpaceInsideHashLiteralBraces: 31 | Enabled: true 32 | EnforcedStyle: no_space 33 | EnforcedStyleForEmptyBraces: no_space 34 | 35 | Layout/LineLength: 36 | Max: 120 37 | Exclude: 38 | - "spec/**/*_spec.rb" 39 | 40 | Lint/AmbiguousBlockAssociation: 41 | Enabled: true 42 | # because 'expect { foo }.to change { bar }' is fine 43 | Exclude: 44 | - "spec/**/*.rb" 45 | 46 | Lint/BooleanSymbol: 47 | Enabled: false 48 | 49 | Lint/MissingSuper: 50 | Enabled: false 51 | 52 | Lint/ConstantDefinitionInBlock: 53 | Enabled: false 54 | 55 | Lint/EmptyBlock: 56 | Exclude: 57 | - "spec/**/*.rb" 58 | 59 | Lint/EmptyClass: 60 | Exclude: 61 | - "spec/**/*.rb" 62 | 63 | Lint/RaiseException: 64 | Enabled: false 65 | 66 | Lint/StructNewOverride: 67 | Enabled: false 68 | 69 | Lint/SuppressedException: 70 | Exclude: 71 | - "spec/**/*.rb" 72 | 73 | Lint/UselessMethodDefinition: 74 | Exclude: 75 | - "lib/rom/struct.rb" 76 | 77 | Lint/UnderscorePrefixedVariableName: 78 | Enabled: false 79 | 80 | Lint/ToEnumArguments: 81 | Exclude: 82 | - "lib/rom/command.rb" 83 | 84 | Naming/MethodParameterName: 85 | Enabled: false 86 | 87 | Naming/AccessorMethodName: 88 | Enabled: false 89 | 90 | Naming/VariableNumber: 91 | Enabled: false 92 | 93 | Naming/PredicateName: 94 | Enabled: false 95 | 96 | Naming/FileName: 97 | Exclude: 98 | - "lib/*-*.rb" 99 | 100 | Naming/MethodName: 101 | Enabled: false 102 | 103 | Naming/MemoizedInstanceVariableName: 104 | Enabled: false 105 | 106 | Metrics/MethodLength: 107 | Enabled: false 108 | 109 | Metrics/ModuleLength: 110 | Enabled: false 111 | 112 | Metrics/ClassLength: 113 | Enabled: false 114 | 115 | Metrics/BlockLength: 116 | Enabled: false 117 | 118 | Metrics/AbcSize: 119 | Max: 36 120 | 121 | Metrics/CyclomaticComplexity: 122 | Enabled: true 123 | Max: 12 124 | 125 | Metrics/PerceivedComplexity: 126 | Max: 14 127 | 128 | Metrics/ParameterLists: 129 | Enabled: false 130 | 131 | Style/AccessorGrouping: 132 | Enabled: false 133 | 134 | Style/ExponentialNotation: 135 | Enabled: false 136 | 137 | Style/HashEachMethods: 138 | Enabled: false 139 | 140 | Style/HashTransformKeys: 141 | Enabled: false 142 | 143 | Style/HashTransformValues: 144 | Enabled: false 145 | 146 | Style/AccessModifierDeclarations: 147 | Enabled: false 148 | 149 | Style/Alias: 150 | Enabled: true 151 | EnforcedStyle: prefer_alias_method 152 | 153 | Style/AsciiComments: 154 | Enabled: false 155 | 156 | Style/BlockDelimiters: 157 | Enabled: false 158 | 159 | Style/ClassAndModuleChildren: 160 | Enabled: false 161 | 162 | Style/ConditionalAssignment: 163 | Enabled: false 164 | 165 | Style/DateTime: 166 | Enabled: false 167 | 168 | Style/Documentation: 169 | Enabled: false 170 | 171 | Style/EachWithObject: 172 | Enabled: false 173 | 174 | Style/FormatString: 175 | Enabled: false 176 | 177 | Style/GuardClause: 178 | Enabled: false 179 | 180 | Style/IfUnlessModifier: 181 | Enabled: false 182 | 183 | Style/Lambda: 184 | Enabled: false 185 | 186 | Style/LambdaCall: 187 | Enabled: false 188 | 189 | Style/ParallelAssignment: 190 | Enabled: false 191 | 192 | Style/StabbyLambdaParentheses: 193 | Enabled: false 194 | 195 | Style/StringLiterals: 196 | Enabled: true 197 | EnforcedStyle: double_quotes 198 | ConsistentQuotesInMultiline: false 199 | 200 | Style/StringLiteralsInInterpolation: 201 | Enabled: true 202 | EnforcedStyle: double_quotes 203 | 204 | Style/SymbolArray: 205 | Enabled: false 206 | 207 | Style/OptionalBooleanParameter: 208 | Enabled: false 209 | 210 | Style/MultilineBlockChain: 211 | Enabled: false 212 | 213 | Style/DocumentDynamicEvalDefinition: 214 | Enabled: false 215 | 216 | Style/TrailingUnderscoreVariable: 217 | Enabled: false 218 | 219 | Style/MultipleComparison: 220 | Enabled: false 221 | 222 | Style/StringConcatenation: 223 | Enabled: false 224 | 225 | Style/OpenStructUse: 226 | Enabled: false 227 | 228 | Style/MapToHash: 229 | Enabled: false 230 | 231 | Style/FormatStringToken: 232 | Enabled: false 233 | 234 | Style/StructInheritance: 235 | Enabled: false 236 | 237 | Style/PreferredHashMethods: 238 | Enabled: false 239 | 240 | Style/DoubleNegation: 241 | Enabled: false 242 | 243 | Style/MissingRespondToMissing: 244 | Enabled: false 245 | 246 | Style/CombinableLoops: 247 | Enabled: false 248 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.10.0 2022-11-26 3 | 4 | ### Changed 5 | - Bump dry-core to ~> 1.0 (via #52) (@tiev) 6 | 7 | [Compare v0.9.0...v0.10.0](https://github.com/rom-rb/rom-http/compare/v0.9.0...v0.10.0) 8 | 9 | ## 0.9.0 2022-09-27 10 | 11 | ### Added 12 | 13 | - Support for associations :tada: (@ianks) 14 | - Support for ruby 3 (@AMHOL) 15 | 16 | ### Changed 17 | - Request params separated into query_params and body_params (@AMHOL in [#44](https://github.com/rom-rb/rom-http/pull/44)) 18 | - Minimal ruby version is set to 2.7 (@flash-gordon) 19 | 20 | ### Fixed 21 | 22 | - Warning message from dry-configurable (@gkostin1966) 23 | 24 | [Compare v0.8.0...v0.9.0](https://github.com/rom-rb/rom-http/compare/v0.8.0...v0.9.0) 25 | 26 | ## 0.8.0 2019-04-29 27 | 28 | As a consequence of these changes, with this release you can easily use `rom-http` along with repositories and changesets. 29 | 30 | ### Added 31 | 32 | - Support for relation schemas (solnic) 33 | - Support for auto-struct mapping (solnic) 34 | - Support for registering your own request/response handlers for all datasets from a specific gateway (solnic) 35 | - Built-in `JSON` handlers that you can set via `handlers: :json` gateway option (solnic) 36 | - Convenient request method predicates `Dataset#{get?,post?,delete?,put?}` (solnic) 37 | 38 | ### Fixed 39 | 40 | - `Relation#append_path` no longer duplicates `base_path` (solnic) 41 | 42 | - Add Dataset#add_params(AMHOL) 43 | 44 | - Updated error messages (AMHOL) 45 | - updated to depend on ROM 4.0 (maximderbin) 46 | - Removed ruby 2.1 support (maximderbin) 47 | - Removed rbx-3 support (maximderbin) 48 | 49 | - Make schemas mandatory and use schema API from ROM core (AMHOL) 50 | - Generate transformer using schema (AMHOL) 51 | - Removed rbx-2 support (solnic) 52 | - Add Dataset#base_path and Relation#primary_key (AMHOL) 53 | - Updated to work with ROM 3.0 (maximderbin) 54 | - Removed ruby 2.1 support (maximderbin) 55 | - Fix Relation#primary_key when schema defines alias (AMHOL) 56 | - Transform keys on insert and update (maximderbin) 57 | - Remove `rom-support` dependency (maximderbin) 58 | 59 | - Removed ruby 2.0 support 60 | - Use `schema` API from ROM::Core. Replaces old schema (solnic) 61 | - Now works out of the box with `rom-repository` (solnic) 62 | 63 | - updated to depend on the forthcoming rom 2.0 (cflipse) 64 | 65 | - replaced dry-data with dry-types (Nikita Shilnikov ) 66 | 67 | 68 | - schema support using `dry-data` (AMHOL) 69 | 70 | 71 | - Projections (`container.relation(:users).project(:id, :name)`) (AMHOL) 72 | 73 | 74 | - `ROM::HTTP::Dataset` macros for setting `default_request_handler` and `default_response_handler` (AMHOL) 75 | 76 | ### Changed 77 | 78 | - Updated to work with `rom ~> 5.0` (parndt) 79 | - Input/output data are now handled by core functionality using schema's `input_schema` and `output_schema` (solnic) 80 | - `Dataset#name` was removed in favor of `Dataset#base_path` 81 | 82 | [Compare v0.7.0...v0.8.0](https://github.com/rom-rb/rom-http/compare/v0.7.0...v0.8.0) 83 | 84 | ## 0.7.0 2018-01-11 85 | 86 | 87 | 88 | [Compare v0.6.0...v0.7.0](https://github.com/rom-rb/rom-http/compare/v0.6.0...v0.7.0) 89 | 90 | ## 0.6.0 2017-02-06 91 | 92 | 93 | 94 | [Compare v0.5.0...v0.6.0](https://github.com/rom-rb/rom-http/compare/v0.5.0...v0.6.0) 95 | 96 | ## 0.5.0 2016-08-08 97 | 98 | 99 | 100 | [Compare v0.4.0...v0.5.0](https://github.com/rom-rb/rom-http/compare/v0.4.0...v0.5.0) 101 | 102 | ## 0.4.0 2016-04-30 103 | 104 | 105 | 106 | [Compare v0.3.0...v0.4.0](https://github.com/rom-rb/rom-http/compare/v0.3.0...v0.4.0) 107 | 108 | ## 0.3.0 2016-03-17 109 | 110 | 111 | 112 | [Compare v0.2.0...v0.3.0](https://github.com/rom-rb/rom-http/compare/v0.2.0...v0.3.0) 113 | 114 | ## 0.2.0 2016-01-30 115 | 116 | 117 | 118 | [Compare v0.1.2...v0.2.0](https://github.com/rom-rb/rom-http/compare/v0.1.2...v0.2.0) 119 | 120 | ## 0.1.2 2015-09-16 121 | 122 | 123 | 124 | [Compare v0.1.1...v0.1.2](https://github.com/rom-rb/rom-http/compare/v0.1.1...v0.1.2) 125 | 126 | ## 0.1.1 2015-09-03 127 | 128 | 129 | ### Changed 130 | 131 | - `ROM::HTTP::Gateway` tries to load the `Dataset` class lazily from the same namespace that the `Gateway` is defined in, with a fallback to `ROM::HTTP::Dataset`, making extending easier (AMHOL) 132 | - `ROM::HTTP::Gateway` no longer raises errors on missing configuration keys, these are now raised late in `Dataset` - this was to allow for the implementation of `default_request_handler` and `default_response_handler` (AMHOL) 133 | - `ROM::HTTP::Dataset` now uses `ROM::Options` from `rom-support`, adding typechecking to options and making it easier to define additional options in extensions 134 | 135 | [Compare v0.1.0...v0.1.1](https://github.com/rom-rb/rom-http/compare/v0.1.0...v0.1.1) 136 | 137 | ## 0.1.0 2015-08-19 138 | 139 | First public release \o/ 140 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @solnic 2 | -------------------------------------------------------------------------------- /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.4.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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue Guidelines 2 | 3 | ## Reporting bugs 4 | 5 | If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. 6 | 7 | ## Reporting feature requests 8 | 9 | Report a feature request **only after discussing it first on [discourse.rom-rb.org](https://discourse.rom-rb.org)** where it was accepted. Please provide a concise description of the feature. 10 | 11 | ## Reporting questions, support requests, ideas, concerns etc. 12 | 13 | **PLEASE DON'T** - use [discourse.rom-rb.org](https://discourse.rom-rb.org) instead. 14 | 15 | # Pull Request Guidelines 16 | 17 | A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. 18 | 19 | Other requirements: 20 | 21 | 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 22 | 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 23 | 3) Add API documentation if it's a new feature 24 | 4) Update API documentation if it changes an existing feature 25 | 5) Bonus points for sending a PR which updates user documentation in the `docsite` directory 26 | 27 | # Asking for help 28 | 29 | If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.rom-rb.org](https://discourse.rom-rb.org). 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | eval_gemfile "Gemfile.devtools" 8 | 9 | group :test do 10 | gem "rspec" 11 | gem "rspec-its" 12 | gem "vcr", require: false 13 | gem "webmock", require: false 14 | end 15 | 16 | group :tools do 17 | gem "byebug", platforms: :mri 18 | end 19 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 8 | 9 | group :test do 10 | gem "simplecov", require: false, platforms: :ruby 11 | gem "simplecov-cobertura", require: false, platforms: :ruby 12 | gem "rexml", require: false 13 | 14 | gem "warning" if RUBY_VERSION >= "2.4.0" 15 | end 16 | 17 | group :tools do 18 | # this is the same version that we use on codacy 19 | gem "rubocop", "1.26.1" 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [gem]: https://rubygems.org/gems/rom-http 2 | [actions]: https://github.com/rom-rb/rom-http/actions 3 | [codacy]: https://www.codacy.com/gh/rom-rb/rom-http 4 | [chat]: https://rom-rb.zulipchat.com 5 | [inchpages]: http://inch-ci.org/github/rom-rb/rom-http 6 | 7 | # rom-http [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-%23346b7a.svg)][chat] 8 | 9 | [![Gem Version](https://badge.fury.io/rb/rom-http.svg)][gem] 10 | [![CI Status](https://github.com/rom-rb/rom-http/workflows/ci/badge.svg)][actions] 11 | [![RuboCop](https://github.com/rom-rb/rom-http/actions/workflows/rubocop.yml/badge.svg)](https://github.com/rom-rb/rom-http/actions/workflows/rubocop.yml) 12 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/d37d66bb711d4aeebf5e1c1650aee261)][codacy] 13 | [![Inline docs](http://inch-ci.org/github/rom-rb/rom-http.svg?branch=main)][inchpages] 14 | 15 | ## Links 16 | 17 | * [User documentation](http://rom-rb.org/learn/http) 18 | * [API documentation](http://rubydoc.info/gems/rom-http) 19 | 20 | ## Supported Ruby versions 21 | 22 | This library officially supports the following Ruby versions: 23 | 24 | * MRI >= `2.7` 25 | * ~jruby >= `9.3`~ (awaits Ruby 2.7 compatibility) 26 | 27 | ## License 28 | 29 | See `LICENSE` file. 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec/core/rake_task" 4 | require "bundler/gem_tasks" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: [:spec] 9 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: 0.10.0 3 | summary: 4 | date: "2022-11-26" 5 | fixed: 6 | - "Bump dry-core to ~> 1.0 (via #52) (@tiev)" 7 | - version: 0.9.0 8 | summary: 9 | date: "2022-09-27" 10 | fixed: 11 | - "Warning message from dry-configurable (@gkostin1966)" 12 | added: 13 | - "Support for associations :tada: (@ianks)" 14 | - "Support for ruby 3 (@AMHOL)" 15 | changed: 16 | - "Request params separated into query_params and body_params (@AMHOL in [#44](https://github.com/rom-rb/rom-http/pull/44))" 17 | - "Minimal ruby version is set to 2.7 (@flash-gordon)" 18 | - version: 0.8.0 19 | date: "2019-04-29" 20 | summary: 21 | As a consequence of these changes, with this release you can easily use 22 | `rom-http` along with repositories and changesets. 23 | added: 24 | - Support for relation schemas (solnic) 25 | - Support for auto-struct mapping (solnic) 26 | - Support for registering your own request/response handlers for all datasets from 27 | a specific gateway (solnic) 28 | - "Built-in `JSON` handlers that you can set via `handlers: :json` gateway option 29 | (solnic)" 30 | - Convenient request method predicates `Dataset#{get?,post?,delete?,put?}` (solnic) 31 | changed: 32 | - Updated to work with `rom ~> 5.0` (parndt) 33 | - Input/output data are now handled by core functionality using schema's `input_schema` 34 | and `output_schema` (solnic) 35 | - "`Dataset#name` was removed in favor of `Dataset#base_path`" 36 | fixed: 37 | - |- 38 | `Relation#append_path` no longer duplicates `base_path` (solnic) 39 | 40 | - Add Dataset#add_params(AMHOL) 41 | 42 | - Updated error messages (AMHOL) 43 | - updated to depend on ROM 4.0 (maximderbin) 44 | - Removed ruby 2.1 support (maximderbin) 45 | - Removed rbx-3 support (maximderbin) 46 | 47 | - Make schemas mandatory and use schema API from ROM core (AMHOL) 48 | - Generate transformer using schema (AMHOL) 49 | - Removed rbx-2 support (solnic) 50 | - Add Dataset#base_path and Relation#primary_key (AMHOL) 51 | - Updated to work with ROM 3.0 (maximderbin) 52 | - Removed ruby 2.1 support (maximderbin) 53 | - Fix Relation#primary_key when schema defines alias (AMHOL) 54 | - Transform keys on insert and update (maximderbin) 55 | - Remove `rom-support` dependency (maximderbin) 56 | 57 | - Removed ruby 2.0 support 58 | - Use `schema` API from ROM::Core. Replaces old schema (solnic) 59 | - Now works out of the box with `rom-repository` (solnic) 60 | 61 | - updated to depend on the forthcoming rom 2.0 (cflipse) 62 | 63 | - replaced dry-data with dry-types (Nikita Shilnikov ) 64 | 65 | 66 | - schema support using `dry-data` (AMHOL) 67 | 68 | 69 | - Projections (`container.relation(:users).project(:id, :name)`) (AMHOL) 70 | 71 | 72 | - `ROM::HTTP::Dataset` macros for setting `default_request_handler` and `default_response_handler` (AMHOL) 73 | - version: 0.7.0 74 | date: "2018-01-11" 75 | - version: 0.6.0 76 | date: "2017-02-06" 77 | - version: 0.5.0 78 | date: "2016-08-08" 79 | - version: 0.4.0 80 | date: "2016-04-30" 81 | - version: 0.3.0 82 | date: "2016-03-17" 83 | - version: 0.2.0 84 | date: "2016-01-30" 85 | - version: 0.1.2 86 | date: "2015-09-16" 87 | - version: 0.1.1 88 | date: "2015-09-03" 89 | changed: 90 | - "`ROM::HTTP::Gateway` tries to load the `Dataset` class lazily from the same namespace 91 | that the `Gateway` is defined in, with a fallback to `ROM::HTTP::Dataset`, making 92 | extending easier (AMHOL)" 93 | - "`ROM::HTTP::Gateway` no longer raises errors on missing configuration keys, these 94 | are now raised late in `Dataset` - this was to allow for the implementation of 95 | `default_request_handler` and `default_response_handler` (AMHOL)" 96 | - "`ROM::HTTP::Dataset` now uses `ROM::Options` from `rom-support`, adding typechecking 97 | to options and making it easier to define additional options in extensions" 98 | - version: 0.1.0 99 | date: "2015-08-19" 100 | summary: First public release \o/ 101 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: 6 3 | chapter: HTTP 4 | --- 5 | 6 | $TOC 7 | 1. [Installing](#installing) 8 | 2. [Connecting to an HTTP API](#connection) 9 | 3. [Relations](#connection) 10 | 4. [Handlers](#handlers) 11 | $TOC 12 | 13 | ROM provides an abstract `HTTP` adapter that you can use to build HTTP client libraries. It provides powerful core features that work out-of-the-box. You have access to relation schemas, custom attribute types, aliasing, auto-mapping to structs, using custom struct namespaces and more. 14 | 15 | Requests and responses can be handled by the built-in handlers, or you can register your own. 16 | 17 | ## Installing 18 | 19 | *Depends on:* `ruby v2.4.0` or greater 20 | 21 | To install rom-http add the following to your Gemfile. 22 | 23 | ```ruby 24 | gem 'rom-http', '~> 0.8' 25 | ``` 26 | 27 | Afterwards either load `rom-http` through your bundler setup or manually in your custom 28 | script like so: 29 | 30 | ```ruby 31 | require 'rom-http' 32 | ``` 33 | 34 | Once loaded the http Adapter will register itself with ROM and become available 35 | for immediate use via the `:http` identifier. 36 | 37 | ## Connection 38 | 39 | When you set up an `HTTP` gateway, you need to specify at the URI. Let's say we want to connect to GitHub API. To configure a gateway: 40 | 41 | ```ruby 42 | config = ROM::Configuration.new(:http, uri: "https://api.github.com", handlers: :json) 43 | ``` 44 | 45 | ^INFO 46 | Setting up a gateway assumes that all registered relations will use the `uri` as the `base_path` for all requests. 47 | ^ 48 | 49 | ## Relations 50 | 51 | When you define relations for `HTTP` adapter, you need to specify the schemas as there's no way to infer them. 52 | 53 | ^INFO 54 | By defining schemas you tell `ROM` which attributes you're interested in, everything else will be rejected from the original responses. 55 | ^ 56 | 57 | Here's an example how you could define a relation to fetch organizations from GitHub: 58 | 59 | ```ruby 60 | module GitHub 61 | module Resources 62 | class Organizations < ROM::Relation[:http] 63 | schema(:orgs) do 64 | attribute :id, Types::Integer 65 | attribute :name, Types::String 66 | attribute :created_at, Types::JSON::Time 67 | attribute :updated_at, Types::JSON::Time 68 | end 69 | 70 | def by_name(name) 71 | append_path(name) 72 | end 73 | end 74 | end 75 | end 76 | 77 | config.register_relation(GitHub::Resources::Organizations) 78 | 79 | rom = ROM.container(config) 80 | ``` 81 | 82 | Now we can use our relation to query GitHub API: 83 | 84 | ```ruby 85 | orgs = rom.relations[:orgs] 86 | 87 | orgs.by_name('rom-rb').one 88 | # {:id=>4589832, :name=>"rom-rb", :created_at=>2013-06-01 22:03:54 UTC, :updated_at=>2019-04-03 14:36:48 UTC} 89 | 90 | orgs.with(auto_struct: true).by_name('rom-rb').one 91 | # # 92 | ``` 93 | 94 | ## Handlers 95 | 96 | Request and response handlers can be registered via `ROM::HTTP::Handlers` object: 97 | 98 | ``` ruby 99 | ROM::HTTP::Handlers.register(:my_handlers, 100 | request: MyRequestHandler, 101 | response: MyResponseHandler 102 | ) 103 | ``` 104 | 105 | Then you can use `:my_handlers` when setting up a gateway. 106 | 107 | ^INFO 108 | Your custom handlers must be compatible with the required interface. Refer to [the built-in JSON handlers](https://github.com/rom-rb/rom-http/blob/main/lib/rom/http/handlers/json.rb) to get the idea. 109 | ^ 110 | 111 | ## Learn more 112 | 113 | * [API documentation](https://api.rom-rb.org/rom-http/) 114 | -------------------------------------------------------------------------------- /examples/github.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom" 4 | 5 | rom = ROM.container(:http, uri: "https://api.github.com", handlers: :json) do |config| 6 | config.relation(:orgs) do 7 | schema do 8 | attribute :id, ROM::HTTP::Types::Integer 9 | attribute :name, ROM::HTTP::Types::String 10 | attribute :created_at, ROM::HTTP::Types::Params::Time 11 | attribute :updated_at, ROM::HTTP::Types::Params::Time 12 | end 13 | 14 | def by_name(name) 15 | append_path(name) 16 | end 17 | end 18 | end 19 | 20 | orgs = rom.relations[:orgs] 21 | 22 | orgs.by_name("rom-rb").one 23 | # {:id=>4589832, :name=>"rom-rb", :created_at=>2013-06-01 22:03:54 UTC, :updated_at=>2019-04-03 14:36:48 UTC} 24 | 25 | orgs.with(auto_struct: true).by_name("rom-rb").one 26 | # # 27 | -------------------------------------------------------------------------------- /examples/repository_with_combine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Example has been moved into a spec. 3 | # 4 | # See spec/integration/abstraction/associations_spec.rb for working code for 5 | # associations. 6 | -------------------------------------------------------------------------------- /lib/rom-http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/http" 4 | -------------------------------------------------------------------------------- /lib/rom/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom" 4 | require "rom/http/error" 5 | require "rom/http/commands" 6 | require "rom/http/associations" 7 | require "rom/http/gateway" 8 | require "rom/http/relation" 9 | require "rom/http/version" 10 | 11 | ROM.register_adapter(:http, ROM::HTTP) 12 | -------------------------------------------------------------------------------- /lib/rom/http/associations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/http/associations/many_to_many" 4 | require "rom/http/associations/one_to_many" 5 | require "rom/http/associations/many_to_one" 6 | require "rom/http/associations/one_to_one" 7 | 8 | module ROM 9 | module HTTP 10 | module Associations 11 | class MissingAssociationViewError < Error; end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rom/http/associations/many_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/associations/many_to_many" 4 | 5 | module ROM 6 | module HTTP 7 | module Associations 8 | class ManyToMany < ROM::Associations::ManyToMany 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rom/http/associations/many_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/associations/many_to_one" 4 | 5 | module ROM 6 | module HTTP 7 | module Associations 8 | # ManyToOne implementation 9 | class ManyToOne < ROM::Associations::ManyToOne 10 | def call(target: self.target) 11 | raise MissingAssociationViewError, "must override view" unless view 12 | 13 | schema = target.schema.qualified 14 | relation = target 15 | apply_view(schema, relation) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rom/http/associations/one_to_many.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/associations/one_to_many" 4 | 5 | module ROM 6 | module HTTP 7 | module Associations 8 | # OneToMany implementation 9 | class OneToMany < ROM::Associations::OneToMany 10 | def call(target: self.target) 11 | raise MissingAssociationViewError, "must override view" unless view 12 | 13 | schema = target.schema.qualified 14 | relation = target 15 | apply_view(schema, relation) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rom/http/associations/one_to_one.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/http/associations/one_to_many" 4 | 5 | module ROM 6 | module HTTP 7 | module Associations 8 | class OneToOne < OneToMany 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rom/http/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/attribute" 4 | 5 | module ROM 6 | module HTTP 7 | class Attribute < ROM::Attribute 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/rom/http/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/commands" 4 | require "rom/http/commands/create" 5 | require "rom/http/commands/update" 6 | require "rom/http/commands/delete" 7 | -------------------------------------------------------------------------------- /lib/rom/http/commands/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | module Commands 6 | # HTTP Create command 7 | # 8 | # @api public 9 | class Create < ROM::Commands::Create 10 | adapter :http 11 | 12 | # Submits each of the provided tuples over HTTP post 13 | # 14 | # @api public 15 | def execute(tuples) 16 | Array([tuples]).flatten.map do |tuple| 17 | attributes = input[tuple] 18 | relation.insert(attributes.to_h) 19 | end.to_a 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rom/http/commands/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | module Commands 6 | # HTTP Delete command 7 | # 8 | # @api public 9 | class Delete < ROM::Commands::Delete 10 | adapter :http 11 | 12 | # Sends an HTTP delete to the dataset path 13 | # 14 | # @api public 15 | def execute 16 | relation.delete 17 | end 18 | 19 | def assert_tuple_count 20 | # noop 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rom/http/commands/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | module Commands 6 | # HTTP update command 7 | # 8 | # @api public 9 | class Update < ROM::Commands::Update 10 | adapter :http 11 | 12 | # Submits each of the provided tuples via HTTP put 13 | # 14 | # @api public 15 | def execute(tuples) 16 | Array([tuples]).flatten.map do |tuple| 17 | attributes = input[tuple] 18 | relation.update(attributes.to_h) 19 | end.to_a 20 | end 21 | 22 | def assert_tuple_count 23 | # noop 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rom/http/dataset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | 5 | require "dry/configurable" 6 | require "dry/core/deprecations" 7 | 8 | require "rom/support/memoizable" 9 | require "rom/constants" 10 | require "rom/initializer" 11 | require "rom/http/types" 12 | require "rom/http/transformer" 13 | 14 | module ROM 15 | module HTTP 16 | # HTTP Dataset 17 | # 18 | # Represents a specific HTTP collection resource. This class can be 19 | # subclassed in a specialized HTTP adapter to provide its own 20 | # response/request handlers or any other configuration that should 21 | # differ from the defaults. 22 | # 23 | # @api public 24 | class Dataset 25 | PATH_SEPARATOR = "/" 26 | 27 | extend Dry::Configurable 28 | extend ROM::Initializer 29 | 30 | include ROM::Memoizable 31 | include Enumerable 32 | include Dry::Equalizer(:options) 33 | 34 | # @!method self.default_request_handler 35 | # Return configured default request handler 36 | # 37 | # @example 38 | # class MyDataset < ROM::HTTP::Dataset 39 | # configure do |config| 40 | # config.default_request_handler = MyRequestHandler 41 | # end 42 | # end 43 | # 44 | # MyDataset.default_request_handler # MyRequestHandler 45 | # MyDataset.new(uri: "http://localhost").request_handler # MyRequestHandler 46 | setting :default_request_handler, reader: true 47 | 48 | # @!method self.default_response_handler 49 | # Return configured default response handler 50 | # 51 | # @example 52 | # class MyDataset < ROM::HTTP::Dataset 53 | # configure do |config| 54 | # config.default_response_handler = MyResponseHandler 55 | # end 56 | # end 57 | # 58 | # MyDataset.default_response_handler # MyResponseHandler 59 | # MyDataset.new(uri: "http://localhost").response_handler # MyResponseHandler 60 | setting :default_response_handler, reader: true 61 | 62 | # @!method self.query_param_encoder 63 | # Return configured query param encoder 64 | # 65 | # @example 66 | # class MyDataset < ROM::HTTP::Dataset 67 | # configure do |config| 68 | # config.query_param_encoder = MyParamEncoder 69 | # end 70 | # end 71 | # 72 | # MyDataset.query_param_encoder # MyParamEncoder 73 | # MyDataset.new(uri: "http://localhost").query_param_encoder # MyParamEncoder 74 | setting :query_param_encoder, default: URI.method(:encode_www_form), reader: true 75 | 76 | # @!attribute [r] request_handler 77 | # @return [Object] 78 | # @api public 79 | option :request_handler, default: proc { self.class.default_request_handler } 80 | 81 | # @!attribute [r] response_handler 82 | # @return [Object] 83 | # @api public 84 | option :response_handler, default: proc { self.class.default_response_handler } 85 | 86 | # @!attribute [r] request_method 87 | # @return [Symbol] 88 | # @api public 89 | option :request_method, type: Types::Symbol, default: proc { :get } 90 | 91 | # @!attribute [r] base_path 92 | # @return [String] 93 | # @api public 94 | option :base_path, type: Types::Path, default: proc { EMPTY_STRING } 95 | 96 | # @!attribute [r] path 97 | # @return [String] 98 | # @api public 99 | option :path, type: Types::Path, default: proc { EMPTY_STRING } 100 | 101 | # @!attribute [r] query_params 102 | # @return [Hash] 103 | # @api public 104 | option :query_params, type: Types::Hash, default: proc { EMPTY_HASH } 105 | 106 | # @!attribute [r] body_params 107 | # @return [Hash] 108 | # @api public 109 | option :body_params, type: Types::Hash, default: proc { EMPTY_HASH } 110 | 111 | # @!attribute [r] headers 112 | # @return [Hash] 113 | # @api public 114 | option :headers, type: Types::Hash, default: proc { EMPTY_HASH } 115 | 116 | # @!attribute [r] headers 117 | # @return [Hash] 118 | # @api public 119 | option :query_param_encoder, default: proc { self.class.query_param_encoder } 120 | 121 | # @!attribute [r] uri 122 | # @return [String] 123 | # @api public 124 | option :uri, type: Types::String 125 | 126 | # Return the dataset's URI 127 | # 128 | # @return [URI::HTTP] 129 | # 130 | # @api public 131 | def uri 132 | uri = URI(join_path(super, path)) 133 | 134 | if query_params.any? 135 | uri.query = query_param_encoder.call(query_params) 136 | end 137 | 138 | uri 139 | end 140 | 141 | # Return true if request method is set to :get 142 | # 143 | # @return [Boolean] 144 | # 145 | # @api public 146 | def get? 147 | request_method.equal?(:get) 148 | end 149 | 150 | # Return true if request method is set to :post 151 | # 152 | # @return [Boolean] 153 | # 154 | # @api public 155 | def post? 156 | request_method.equal?(:post) 157 | end 158 | 159 | # Return true if request method is set to :put 160 | # 161 | # @return [Boolean] 162 | # 163 | # @api public 164 | def put? 165 | request_method.equal?(:put) 166 | end 167 | 168 | # Return true if request method is set to :delete 169 | # 170 | # @return [Boolean] 171 | # 172 | # @api public 173 | def delete? 174 | request_method.equal?(:delete) 175 | end 176 | 177 | # Return the dataset path 178 | # 179 | # @example 180 | # Dataset.new(path: '/users').path 181 | # # => 'users' 182 | # 183 | # @return [String] the dataset path, without a leading slash 184 | # 185 | # @api public 186 | def path 187 | join_path(base_path, super) 188 | end 189 | 190 | # Return the dataset path 191 | # 192 | # @example 193 | # Dataset.new(path: '/users').path 194 | # # => '/users' 195 | # 196 | # @return [String] the dataset path, with leading slash 197 | # 198 | # @api public 199 | def absolute_path 200 | PATH_SEPARATOR + path 201 | end 202 | 203 | # Return a new dataset with given headers 204 | # 205 | # @param headers [Hash] The new headers 206 | # 207 | # @note this _replaces_ the dataset's currently configured headers. 208 | # To non-destructively add a new header, use `#add_header` 209 | # 210 | # @example 211 | # users = Dataset.new(headers: { Accept: 'application/json' }) 212 | # users.with_headers(:'X-Api-Key' => '1234').headers 213 | # # => { :'X-Api-Key' => '1234' } 214 | # 215 | # @return [Dataset] 216 | # 217 | # @api public 218 | def with_headers(headers) 219 | with_options(headers: headers) 220 | end 221 | 222 | # Return a new dataset with additional header 223 | # 224 | # @param header [Symbol] the HTTP header to add 225 | # @param value [String] the header value 226 | # 227 | # @example 228 | # users = Dataset.new(headers: { Accept: 'application/json' }) 229 | # users.add_header(:'X-Api-Key', '1234').headers 230 | # # => { :Accept => 'application/json', :'X-Api-Key' => '1234' } 231 | # 232 | # @return [Dataset] 233 | # 234 | # @api public 235 | def add_header(header, value) 236 | with_headers(headers.merge(header => value)) 237 | end 238 | 239 | # Return a new dataset with additional options 240 | # 241 | # @param opts [Hash] the new options to add 242 | # 243 | # @return [Dataset] 244 | # 245 | # @api public 246 | def with_options(opts) 247 | __new__(**options.merge(opts)) 248 | end 249 | 250 | # Return a new dataset with a different base path 251 | # 252 | # @param base_path [String] the new base request path 253 | # 254 | # @example 255 | # users.with_base_path('/profiles').base_path 256 | # # => 'profiles' 257 | # 258 | # @return [Dataset] 259 | # 260 | # @api public 261 | def with_base_path(base_path) 262 | with_options(base_path: base_path) 263 | end 264 | 265 | # Return a new dataset with a different path 266 | # 267 | # @param path [String] the new request path 268 | # 269 | # @example 270 | # users.with_path('/profiles').path 271 | # # => 'profiles' 272 | # 273 | # @return [Dataset] 274 | # 275 | # @api public 276 | def with_path(path) 277 | with_options(path: path) 278 | end 279 | 280 | # Return a new dataset with a modified path 281 | # 282 | # @param path [String] new path fragment 283 | # 284 | # @example 285 | # users.append_path('profiles').path 286 | # # => users/profiles 287 | # 288 | # @return [Dataset] 289 | # 290 | # @api public 291 | def append_path(append_path) 292 | with_path(join_path(options[:path], append_path)) 293 | end 294 | 295 | # Return a new dataset with a different request method 296 | # 297 | # @param [Symbol] request_method the new HTTP verb 298 | # 299 | # @example 300 | # users.request_method(:put) 301 | # 302 | # @return [Dataset] 303 | # 304 | # @api public 305 | def with_request_method(request_method) 306 | with_options(request_method: request_method) 307 | end 308 | 309 | # Return a new dataset with replaced request query parameters 310 | # 311 | # @param [Hash] query_params the new request query parameters 312 | # 313 | # @example 314 | # users = Dataset.new(query_params: { uid: 33 }) 315 | # users.with_query_params(login: 'jdoe').query_params 316 | # # => { :login => 'jdoe' } 317 | # 318 | # @return [Dataset] 319 | # 320 | # @api public 321 | def with_query_params(query_params) 322 | with_options(query_params: query_params) 323 | end 324 | 325 | # Return a new dataset with merged request query parameters 326 | # 327 | # @param [Hash] query_params the new request query parameters to add 328 | # 329 | # @example 330 | # users = Dataset.new(query_params: { uid: 33 }) 331 | # users.add_query_params(login: 'jdoe').query_params 332 | # # => { uid: 33, :login => 'jdoe' } 333 | # 334 | # @return [Dataset] 335 | # 336 | # @api public 337 | def add_query_params(new_query_params) 338 | with_options(query_params: ::ROM::HTTP::Transformer[:deep_merge][query_params, 339 | new_query_params]) 340 | end 341 | 342 | # Return a new dataset with replaced request body parameters 343 | # 344 | # @param [Hash] body_params the new request body parameters 345 | # 346 | # @example 347 | # users = Dataset.new(body_params: { uid: 33 }) 348 | # users.with_body_params(login: 'jdoe').body_params 349 | # # => { :login => 'jdoe' } 350 | # 351 | # @return [Dataset] 352 | # 353 | # @api public 354 | def with_body_params(body_params) 355 | with_options(body_params: body_params) 356 | end 357 | 358 | # Return a new dataset with merged request body parameters 359 | # 360 | # @param [Hash] body_params the new request body parameters to add 361 | # 362 | # @example 363 | # users = Dataset.new(body_params: { uid: 33 }) 364 | # users.add_body_params(login: 'jdoe').body_params 365 | # # => { uid: 33, :login => 'jdoe' } 366 | # 367 | # @return [Dataset] 368 | # 369 | # @api public 370 | def add_body_params(new_body_params) 371 | with_options(body_params: ::ROM::HTTP::Transformer[:deep_merge][body_params, 372 | new_body_params]) 373 | end 374 | 375 | # Iterate over each response value 376 | # 377 | # @yield [Hash] a dataset tuple 378 | # 379 | # @return [Enumerator] if no block is given 380 | # @return [Array] 381 | # 382 | # @api public 383 | def each(&block) 384 | return to_enum unless block_given? 385 | 386 | response.each(&block) 387 | end 388 | 389 | # Perform an insert over HTTP Post 390 | # 391 | # @param [Hash] attributes the attributes to insert 392 | # 393 | # @return [Array] 394 | # 395 | # @api public 396 | def insert(attributes) 397 | with_options( 398 | request_method: :post, 399 | body_params: attributes 400 | ).response 401 | end 402 | 403 | # Perform an update over HTTP Put 404 | # 405 | # @param [Hash] attributes the attributes to update 406 | # 407 | # @return [Array] 408 | # 409 | # @api public 410 | def update(attributes) 411 | with_options( 412 | request_method: :put, 413 | body_params: attributes 414 | ).response 415 | end 416 | 417 | # Perform an delete over HTTP Delete 418 | # 419 | # 420 | # @return [Array] 421 | # 422 | # @api public 423 | def delete 424 | with_options(request_method: :delete).response 425 | end 426 | 427 | # Execute the current dataset 428 | # 429 | # @return [Array] 430 | # 431 | # @api public 432 | def response 433 | response_handler.call(request_handler.call(self), self) 434 | end 435 | 436 | memoize :uri, :absolute_path 437 | 438 | private 439 | 440 | # @api private 441 | def __new__(*args, **kwargs, &block) 442 | self.class.new(*args, **kwargs, &block) 443 | end 444 | 445 | # @api private 446 | def join_path(*paths) 447 | paths.reject(&:empty?).join(PATH_SEPARATOR) 448 | end 449 | end 450 | end 451 | end 452 | -------------------------------------------------------------------------------- /lib/rom/http/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | Error = Class.new(StandardError) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rom/http/gateway.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent" 4 | 5 | require "rom/http/dataset" 6 | require "rom/http/handlers" 7 | require "rom/http/mapper_compiler" 8 | 9 | module ROM 10 | module HTTP 11 | # HTTP gateway 12 | # 13 | # @example 14 | # config = { 15 | # uri: 'http://jsonplaceholder.typicode.com', 16 | # headers: { Accept: 'applicaiton/json' } 17 | # } 18 | # 19 | # gateway = ROM::HTTP::Gateway.new(config) 20 | # users = gateway.dataset(:users) 21 | # 22 | # @api public 23 | class Gateway < ROM::Gateway 24 | adapter :http 25 | 26 | attr_reader :datasets, :config 27 | 28 | # HTTP gateway interface 29 | # 30 | # @param [Hash] config configuration options 31 | # @option config [String] :uri The base API for the HTTP service 32 | # @option config [Hash] :headers Default request headers 33 | # 34 | # @see Dataset 35 | # 36 | # @api public 37 | def initialize(config) 38 | @datasets = Concurrent::Map.new 39 | @config = config 40 | end 41 | 42 | # Retrieve dataset with the given name 43 | # 44 | # @param [String] name dataaset name 45 | # 46 | # @return [Dataset] 47 | # 48 | # @api public 49 | def [](name) 50 | datasets.fetch(name) 51 | end 52 | 53 | # Build dataset with the given name 54 | # 55 | # @param [String] name dataaset name 56 | # 57 | # @return [Dataset] 58 | # 59 | # @api public 60 | def dataset(name) 61 | datasets[name] = dataset_class.new(**dataset_options(name)) 62 | end 63 | 64 | # Check if dataset exists 65 | # 66 | # @param [String] name dataset name 67 | # 68 | # @api public 69 | def dataset?(name) 70 | datasets.key?(name) 71 | end 72 | 73 | private 74 | 75 | # Return Dataset class 76 | # 77 | # @return [Class] 78 | # 79 | # @api private 80 | def dataset_class 81 | namespace.const_defined?(:Dataset) ? namespace.const_get(:Dataset) : Dataset 82 | end 83 | 84 | # Return Dataset options 85 | # 86 | # @return [Class] 87 | # 88 | # @api private 89 | def dataset_options(name) 90 | config.merge(uri: uri, base_path: name, **default_handlers) 91 | end 92 | 93 | # Return default handlers registered in Handlers registry 94 | # 95 | # @return [Hash] 96 | # 97 | # @api private 98 | def default_handlers 99 | if (handlers_key = config[:handlers]) 100 | Handlers[handlers_key] 101 | .map { |key, value| [:"#{key}_handler", value] }.to_h 102 | else 103 | EMPTY_HASH 104 | end 105 | end 106 | 107 | # @api private 108 | def uri 109 | config.fetch(:uri) { raise Error, "+uri+ configuration missing" } 110 | end 111 | 112 | # @api private 113 | def namespace 114 | self.class.to_s[/(.*)(?=::)/].split("::").inject(::Object) do |constant, const_name| 115 | constant.const_get(const_name) 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/rom/http/handlers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/http/handlers/json" 4 | 5 | module ROM 6 | module HTTP 7 | # Request/response handler registry 8 | # 9 | # @api public 10 | class Handlers 11 | extend Dry::Core::Container::Mixin 12 | 13 | register(:json, request: JSONRequest, response: JSONResponse) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rom/http/handlers/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | require "net/http" 5 | require "json" 6 | 7 | require "rom/support/inflector" 8 | 9 | module ROM 10 | module HTTP 11 | # Default request/response handlers 12 | # 13 | # @api public 14 | class Handlers 15 | # Default handler for JSON requests 16 | # 17 | # @api public 18 | class JSONRequest 19 | # Handle JSON request for the provided dataset 20 | # 21 | # @param [Dataset] dataset 22 | # 23 | # @return [Array] 24 | # 25 | # @api public 26 | def self.call(dataset) 27 | uri = dataset.uri 28 | 29 | http = Net::HTTP.new(uri.host, uri.port) 30 | http.use_ssl = true if uri.scheme.eql?("https") 31 | 32 | request_class = Net::HTTP.const_get(ROM::Inflector.classify(dataset.request_method)) 33 | 34 | request = request_class.new(uri.request_uri) 35 | 36 | dataset.headers.each_with_object(request) do |(header, value), req| 37 | req[header.to_s] = value 38 | end 39 | 40 | request.body = JSON.dump(dataset.body_params) if dataset.body_params.any? 41 | 42 | http.request(request) 43 | end 44 | end 45 | 46 | # Default handler for JSON responses 47 | # 48 | # @api public 49 | class JSONResponse 50 | # Handle JSON responses 51 | # 52 | # @param [Net::HTTP::Response] response 53 | # @param [Dataset] dataset 54 | # 55 | # @return [Array] 56 | # 57 | # @api public 58 | def self.call(response, _dataset) 59 | Array([JSON.parse(response.body)]).flatten(1) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/rom/http/mapper_compiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/mapper_compiler" 4 | 5 | module ROM 6 | module HTTP 7 | class MapperCompiler < ROM::MapperCompiler 8 | mapper_options(reject_keys: true) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rom/http/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/initializer" 4 | 5 | require "rom/http/types" 6 | require "rom/http/attribute" 7 | require "rom/http/schema" 8 | require "rom/http/schema/dsl" 9 | 10 | module ROM 11 | module HTTP 12 | # HTTP-specific relation extensions 13 | # 14 | class Relation < ROM::Relation 15 | include ROM::HTTP 16 | 17 | adapter :http 18 | 19 | schema_class HTTP::Schema 20 | schema_dsl HTTP::Schema::DSL 21 | schema_attr_class HTTP::Attribute 22 | 23 | option :output_schema, default: -> { schema.to_output_hash } 24 | 25 | forward :with_headers, :add_header, :with_options, 26 | :with_base_path, :with_path, :append_path, 27 | :with_request_method, :with_query_params, :add_query_params, 28 | :with_body_params, :add_body_params 29 | 30 | def primary_key 31 | schema.primary_key_name 32 | end 33 | 34 | def project(*names) 35 | with(schema: schema.project(*names.flatten)) 36 | end 37 | 38 | def exclude(*names) 39 | with(schema: schema.exclude(*names)) 40 | end 41 | 42 | def rename(mapping) 43 | with(schema: schema.rename(mapping)) 44 | end 45 | 46 | def prefix(prefix) 47 | with(schema: schema.prefix(prefix)) 48 | end 49 | 50 | # @see Dataset#insert 51 | def insert(*tuples) 52 | dataset.insert(*tuples.map { |t| input_schema[t] }) 53 | end 54 | alias_method :<<, :insert 55 | 56 | # @see Dataset#update 57 | def update(*tuples) 58 | dataset.update(*tuples.map { |t| input_schema[t] }) 59 | end 60 | 61 | # @see Dataset#delete 62 | def delete 63 | dataset.delete 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/rom/http/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/schema" 4 | require "rom/http/types" 5 | 6 | module ROM 7 | module HTTP 8 | class Schema < ROM::Schema 9 | # Customized output hash constructor which symbolizes keys 10 | # and optionally applies custom read-type coercions 11 | # 12 | # @api private 13 | def to_output_hash 14 | Types::Hash 15 | .schema(map { |attr| [attr.key, attr.to_read_type] }.to_h) 16 | .with_key_transform(&:to_sym) 17 | end 18 | 19 | # To maintain compatibility with other adapters 20 | # 21 | # @api private 22 | def qualified 23 | self 24 | end 25 | 26 | # Internal hook used during setup process 27 | # 28 | # @see Schema#finalize_associations! 29 | # 30 | # @api private 31 | def finalize_associations!(relations:) 32 | super do 33 | associations.map do |definition| 34 | HTTP::Associations.const_get(definition.type).new(definition, relations) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/rom/http/schema/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/schema/dsl" 4 | 5 | module ROM 6 | module HTTP 7 | class Schema < ROM::Schema 8 | class DSL < ROM::Schema::DSL 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rom/http/transformer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | # Transformer 6 | # 7 | # Used to perform data transformations on behalf of relations 8 | # 9 | # @api private 10 | module Transformer 11 | extend Transproc::Registry 12 | 13 | import :identity, from: ::Transproc::Coercions 14 | import :map_array, from: ::Transproc::ArrayTransformations 15 | import :rename_keys, from: ::Transproc::HashTransformations 16 | import :deep_merge, from: ::Transproc::HashTransformations 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rom/http/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/types" 4 | 5 | module ROM 6 | module HTTP 7 | module Types 8 | include ROM::Types 9 | 10 | Path = Coercible::String.constructor { |s| s.sub(%r{\A/}, "") } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rom/http/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module HTTP 5 | VERSION = "0.10.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: rom-http 2 | codacy_id: d37d66bb711d4aeebf5e1c1650aee261 3 | -------------------------------------------------------------------------------- /rom-http.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rom/http/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rom-http' 8 | spec.version = ROM::HTTP::VERSION.dup 9 | spec.authors = ['Piotr Solnica', 'Andy Holland', 'Chris Flipse'] 10 | spec.email = ['piotr.solnica@gmail.com', 'andyholland1991@aol.com', 'cflipse@gmail.com'] 11 | spec.summary = 'HTTP support for ROM' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://rom-rb.org' 14 | spec.license = 'MIT' 15 | 16 | spec.files = Dir['CHANGELOG.md', 'LICENSE.txt', 'README.md', 'lib/**/*'] 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | spec.required_ruby_version = '>= 2.7.0' 21 | 22 | spec.add_runtime_dependency 'concurrent-ruby', '~> 1.1' 23 | spec.add_runtime_dependency 'addressable', '~> 2.6' 24 | spec.add_runtime_dependency 'rom', '~> 5.0', '>= 5.0.1' 25 | spec.add_runtime_dependency 'dry-core', '~> 1.0' 26 | spec.add_runtime_dependency 'dry-equalizer', '~> 0.2' 27 | spec.add_runtime_dependency 'dry-configurable', '~> 1.0' 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/github_repos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/orgs/rom-rb 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - GitHub.com 23 | Date: 24 | - Mon, 29 Apr 2019 10:05:07 GMT 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Transfer-Encoding: 28 | - chunked 29 | Status: 30 | - 200 OK 31 | X-Ratelimit-Limit: 32 | - '60' 33 | X-Ratelimit-Remaining: 34 | - '57' 35 | X-Ratelimit-Reset: 36 | - '1556535839' 37 | Cache-Control: 38 | - public, max-age=60, s-maxage=60 39 | Vary: 40 | - Accept 41 | Etag: 42 | - W/"7c4febf9df2293903c92f884a768a81a" 43 | Last-Modified: 44 | - Wed, 03 Apr 2019 14:36:48 GMT 45 | X-Github-Media-Type: 46 | - github.v3; format=json 47 | Access-Control-Expose-Headers: 48 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 49 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 50 | X-GitHub-Media-Type 51 | Access-Control-Allow-Origin: 52 | - "*" 53 | Strict-Transport-Security: 54 | - max-age=31536000; includeSubdomains; preload 55 | X-Frame-Options: 56 | - deny 57 | X-Content-Type-Options: 58 | - nosniff 59 | X-Xss-Protection: 60 | - 1; mode=block 61 | Referrer-Policy: 62 | - origin-when-cross-origin, strict-origin-when-cross-origin 63 | Content-Security-Policy: 64 | - default-src 'none' 65 | X-Github-Request-Id: 66 | - E967:457D:88BBB1:15715A9:5CC6CC52 67 | body: 68 | encoding: ASCII-8BIT 69 | string: '{"login":"rom-rb","id":4589832,"node_id":"MDEyOk9yZ2FuaXphdGlvbjQ1ODk4MzI=","url":"https://api.github.com/orgs/rom-rb","repos_url":"https://api.github.com/orgs/rom-rb/repos","events_url":"https://api.github.com/orgs/rom-rb/events","hooks_url":"https://api.github.com/orgs/rom-rb/hooks","issues_url":"https://api.github.com/orgs/rom-rb/issues","members_url":"https://api.github.com/orgs/rom-rb/members{/member}","public_members_url":"https://api.github.com/orgs/rom-rb/public_members{/member}","avatar_url":"https://avatars3.githubusercontent.com/u/4589832?v=4","description":"Persistence 70 | and mapping toolkit for Ruby","name":"rom-rb","company":null,"blog":"http://rom-rb.org","location":"Planet 71 | Earth","email":"","is_verified":false,"has_organization_projects":true,"has_repository_projects":true,"public_repos":32,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/rom-rb","created_at":"2013-06-01T22:03:54Z","updated_at":"2019-04-03T14:36:48Z","type":"Organization"}' 72 | http_version: 73 | recorded_at: Mon, 29 Apr 2019 10:05:06 GMT 74 | recorded_with: VCR 4.0.0 75 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/posts_and_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.mocki.io/v1/c2037888 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | Content-Length: 24 | - '47' 25 | Connection: 26 | - keep-alive 27 | Date: 28 | - Fri, 26 Feb 2021 18:58:42 GMT 29 | X-Amzn-Requestid: 30 | - dacb5bae-db7a-4f10-abe8-9596dfda0869 31 | Access-Control-Allow-Origin: 32 | - "*" 33 | X-Amzn-Remapped-Content-Length: 34 | - '47' 35 | X-Amz-Apigw-Id: 36 | - bXezHFr1IAMFRrw= 37 | Etag: 38 | - W/"2f-0BK/cHUYh1nqlpFqHbu4AtW03bY" 39 | X-Powered-By: 40 | - Express 41 | X-Amzn-Trace-Id: 42 | - Root=1-603944e0-225ac51059c55d36563406c1;Sampled=0 43 | X-Cache: 44 | - Miss from cloudfront 45 | Via: 46 | - 1.1 744a1f6aa351c9fa7c55143547282de3.cloudfront.net (CloudFront) 47 | X-Amz-Cf-Pop: 48 | - ATL56-C3 49 | X-Amz-Cf-Id: 50 | - 9ghTG0ref7YxoXwYF37mJ519f-xL2OPaJdKXhGrIpXcUv5bFAexkHw== 51 | body: 52 | encoding: UTF-8 53 | string: '[{"id":1,"name":"John"},{"id":2,"name":"Jill"}]' 54 | recorded_at: Fri, 26 Feb 2021 18:58:42 GMT 55 | - request: 56 | method: get 57 | uri: https://api.mocki.io/v1/2ac7da28 58 | body: 59 | encoding: US-ASCII 60 | string: '' 61 | headers: 62 | Accept-Encoding: 63 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 64 | Accept: 65 | - application/json 66 | User-Agent: 67 | - Ruby 68 | response: 69 | status: 70 | code: 200 71 | message: OK 72 | headers: 73 | Content-Type: 74 | - application/json; charset=utf-8 75 | Content-Length: 76 | - '77' 77 | Connection: 78 | - keep-alive 79 | Date: 80 | - Fri, 26 Feb 2021 18:58:44 GMT 81 | X-Amzn-Requestid: 82 | - 79be4182-a695-478b-9dc4-c1ca7048b33c 83 | Access-Control-Allow-Origin: 84 | - "*" 85 | X-Amzn-Remapped-Content-Length: 86 | - '77' 87 | X-Amz-Apigw-Id: 88 | - bXezZHTnIAMFhyg= 89 | Etag: 90 | - W/"4d-yap42SJ8nfd/KHL17f3ZzAAdBgs" 91 | X-Powered-By: 92 | - Express 93 | X-Amzn-Trace-Id: 94 | - Root=1-603944e2-03bbe2b779107a1b6cd59121;Sampled=0 95 | X-Cache: 96 | - Miss from cloudfront 97 | Via: 98 | - 1.1 2c65bede0f52554bf32fd64f7dfa83d0.cloudfront.net (CloudFront) 99 | X-Amz-Cf-Pop: 100 | - ATL56-C3 101 | X-Amz-Cf-Id: 102 | - ObonJUYm0INVEJxtLm0cH4REaUXpnoyr9tQRyy1MBqxxeKLDkWU-kg== 103 | body: 104 | encoding: UTF-8 105 | string: '[{"id":1,"title":"Post 1","user_id":1},{"id":2,"title":"Post 2","user_id":2}]' 106 | recorded_at: Fri, 26 Feb 2021 18:58:44 GMT 107 | recorded_with: VCR 6.0.0 108 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/posts_with_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.mocki.io/v1/2ac7da28 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | Content-Length: 24 | - '77' 25 | Connection: 26 | - keep-alive 27 | Date: 28 | - Fri, 26 Feb 2021 19:01:31 GMT 29 | X-Amzn-Requestid: 30 | - 11fa901d-b43d-4fc7-880a-ad4361ee6800 31 | Access-Control-Allow-Origin: 32 | - "*" 33 | X-Amzn-Remapped-Content-Length: 34 | - '77' 35 | X-Amz-Apigw-Id: 36 | - bXfNoECVIAMFuNw= 37 | Etag: 38 | - W/"4d-yap42SJ8nfd/KHL17f3ZzAAdBgs" 39 | X-Powered-By: 40 | - Express 41 | X-Amzn-Trace-Id: 42 | - Root=1-6039458a-6b5b9d9b06c42a971ba11e98;Sampled=0 43 | X-Cache: 44 | - Miss from cloudfront 45 | Via: 46 | - 1.1 08510eafc0e0ea8e840045ca17ba8a6d.cloudfront.net (CloudFront) 47 | X-Amz-Cf-Pop: 48 | - ATL56-C3 49 | X-Amz-Cf-Id: 50 | - AoxZy6_PvPchyVdnV08y0ncIpfc_2QL0gwGnRdSuMeIkZ9UhqtsBvA== 51 | body: 52 | encoding: UTF-8 53 | string: '[{"id":1,"title":"Post 1","user_id":1},{"id":2,"title":"Post 2","user_id":2}]' 54 | recorded_at: Fri, 26 Feb 2021 19:01:31 GMT 55 | - request: 56 | method: get 57 | uri: https://api.mocki.io/v1/c2037888 58 | body: 59 | encoding: US-ASCII 60 | string: '' 61 | headers: 62 | Accept-Encoding: 63 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 64 | Accept: 65 | - application/json 66 | User-Agent: 67 | - Ruby 68 | response: 69 | status: 70 | code: 200 71 | message: OK 72 | headers: 73 | Content-Type: 74 | - application/json; charset=utf-8 75 | Content-Length: 76 | - '47' 77 | Connection: 78 | - keep-alive 79 | Date: 80 | - Fri, 26 Feb 2021 19:01:33 GMT 81 | X-Amzn-Requestid: 82 | - 0ad72e00-cd41-4f21-a8e8-0c496ae856e1 83 | Access-Control-Allow-Origin: 84 | - "*" 85 | X-Amzn-Remapped-Content-Length: 86 | - '47' 87 | X-Amz-Apigw-Id: 88 | - bXfN5Gm9oAMFogA= 89 | Etag: 90 | - W/"2f-0BK/cHUYh1nqlpFqHbu4AtW03bY" 91 | X-Powered-By: 92 | - Express 93 | X-Amzn-Trace-Id: 94 | - Root=1-6039458c-39d737991b07af4a2bc75cdb;Sampled=0 95 | X-Cache: 96 | - Miss from cloudfront 97 | Via: 98 | - 1.1 915092a962e63573a018390dfbdde6a5.cloudfront.net (CloudFront) 99 | X-Amz-Cf-Pop: 100 | - ATL56-C3 101 | X-Amz-Cf-Id: 102 | - Gt0PBK3aLSgWgTr9HZ9duv4qVu279OCqMNeehN4S0NKue1I5kq89xw== 103 | body: 104 | encoding: UTF-8 105 | string: '[{"id":1,"name":"John"},{"id":2,"name":"Jill"}]' 106 | recorded_at: Fri, 26 Feb 2021 19:01:33 GMT 107 | recorded_with: VCR 6.0.0 108 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/user_with_posts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.mocki.io/v1/c2037888 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Content-Type: 22 | - application/json; charset=utf-8 23 | Content-Length: 24 | - '47' 25 | Connection: 26 | - keep-alive 27 | Date: 28 | - Fri, 26 Feb 2021 19:02:47 GMT 29 | X-Amzn-Requestid: 30 | - b824ccb7-0112-4a9b-a70c-bd24bd5e8122 31 | Access-Control-Allow-Origin: 32 | - "*" 33 | X-Amzn-Remapped-Content-Length: 34 | - '47' 35 | X-Amz-Apigw-Id: 36 | - bXfZfHTdoAMFgtw= 37 | Etag: 38 | - W/"2f-0BK/cHUYh1nqlpFqHbu4AtW03bY" 39 | X-Powered-By: 40 | - Express 41 | X-Amzn-Trace-Id: 42 | - Root=1-603945d6-545cfc7856bbde82139ed703;Sampled=0 43 | X-Cache: 44 | - Miss from cloudfront 45 | Via: 46 | - 1.1 e779e6690108fc19727694cd1f90461b.cloudfront.net (CloudFront) 47 | X-Amz-Cf-Pop: 48 | - ATL56-C3 49 | X-Amz-Cf-Id: 50 | - F6CGyLBvzRjHqm-bluTKNRSVCj5lZgnqi6_JuWhs21jhBX-a_8BObg== 51 | body: 52 | encoding: UTF-8 53 | string: '[{"id":1,"name":"John"},{"id":2,"name":"Jill"}]' 54 | recorded_at: Fri, 26 Feb 2021 19:02:47 GMT 55 | - request: 56 | method: get 57 | uri: https://api.mocki.io/v1/2ac7da28 58 | body: 59 | encoding: US-ASCII 60 | string: '' 61 | headers: 62 | Accept-Encoding: 63 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 64 | Accept: 65 | - application/json 66 | User-Agent: 67 | - Ruby 68 | response: 69 | status: 70 | code: 200 71 | message: OK 72 | headers: 73 | Content-Type: 74 | - application/json; charset=utf-8 75 | Content-Length: 76 | - '77' 77 | Connection: 78 | - keep-alive 79 | Date: 80 | - Fri, 26 Feb 2021 19:02:49 GMT 81 | X-Amzn-Requestid: 82 | - cae1ea96-76b2-4f6a-b6b9-2f07f9f18a91 83 | Access-Control-Allow-Origin: 84 | - "*" 85 | X-Amzn-Remapped-Content-Length: 86 | - '77' 87 | X-Amz-Apigw-Id: 88 | - bXfZtEw1oAMFzvw= 89 | Etag: 90 | - W/"4d-yap42SJ8nfd/KHL17f3ZzAAdBgs" 91 | X-Powered-By: 92 | - Express 93 | X-Amzn-Trace-Id: 94 | - Root=1-603945d7-237eb7971fc4276d124ca55e;Sampled=0 95 | X-Cache: 96 | - Miss from cloudfront 97 | Via: 98 | - 1.1 df6e44b3609b247c2f17e18f40a0e485.cloudfront.net (CloudFront) 99 | X-Amz-Cf-Pop: 100 | - ATL56-C3 101 | X-Amz-Cf-Id: 102 | - Z6pVTmVTENbkP6VI5KfMekrZsJbOgzez_olZPZ2HND1t3cFpHQbJ4g== 103 | body: 104 | encoding: UTF-8 105 | string: '[{"id":1,"title":"Post 1","user_id":1},{"id":2,"title":"Post 2","user_id":2}]' 106 | recorded_at: Fri, 26 Feb 2021 19:02:49 GMT 107 | recorded_with: VCR 6.0.0 108 | -------------------------------------------------------------------------------- /spec/integration/abstract/associations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Associations", :vcr do 4 | let(:configuration) do 5 | configuration = ROM::Configuration.new( 6 | :http, { 7 | uri: "https://api.mocki.io", 8 | headers: { 9 | Accept: "application/json" 10 | }, 11 | handlers: :json 12 | } 13 | ) 14 | configuration.register_relation(users_klass) 15 | configuration.register_relation(posts_klass) 16 | ROM.container(configuration) 17 | end 18 | 19 | let(:users) { configuration.relations[:users] } 20 | 21 | let(:posts) { configuration.relations[:posts] } 22 | 23 | let(:users_klass) do 24 | Class.new(ROM::HTTP::Relation) do 25 | dataset do 26 | with_options(base_path: "v1/c2037888") 27 | end 28 | 29 | schema(:users) do 30 | attribute :id, ROM::Types::Integer.meta(primary_key: true) 31 | attribute :name, ROM::Types::String 32 | 33 | associations do 34 | has_many :posts, view: :for_users, override: true 35 | end 36 | end 37 | 38 | def for_posts(_assoc, posts) 39 | # IRL, you would filter the users with post id params in the API 40 | _post_ids = posts.map { |u| u[:id] } 41 | self 42 | end 43 | end 44 | end 45 | 46 | let(:posts_klass) do 47 | Class.new(ROM::HTTP::Relation) do 48 | dataset do 49 | with_options(base_path: "v1/2ac7da28") 50 | end 51 | 52 | schema(:posts) do 53 | attribute :id, ROM::Types::Integer.meta(primary_key: true) 54 | attribute :user_id, ROM::Types::Integer.meta(foreign_key: true) 55 | attribute :title, ROM::Types::String 56 | 57 | associations do 58 | belongs_to :user, view: :for_posts, override: true 59 | end 60 | end 61 | 62 | def for_users(_assoc, users) 63 | # IRL, you would filter the posts with user id params in the API 64 | _user_ids = users.map { |u| u[:id] } 65 | self 66 | end 67 | end 68 | end 69 | 70 | describe "has_many" do 71 | it "can combine results" do 72 | VCR.use_cassette(:user_with_posts) do 73 | result = users.combine(:posts).to_a 74 | 75 | expect(result).to contain_exactly( 76 | {id: 1, name: "John", posts: [{id: 1, user_id: 1, title: "Post 1"}]}, 77 | {id: 2, name: "Jill", posts: [{id: 2, user_id: 2, title: "Post 2"}]} 78 | ) 79 | end 80 | end 81 | end 82 | 83 | describe "belongs_to" do 84 | it "can combine results" do 85 | VCR.use_cassette(:posts_with_user) do 86 | result = posts.combine(:user).to_a 87 | 88 | expect(result).to contain_exactly( 89 | {id: 1, title: "Post 1", user: {id: 1, name: "John"}, user_id: 1}, 90 | {id: 2, title: "Post 2", user: {id: 2, name: "Jill"}, user_id: 2} 91 | ) 92 | end 93 | end 94 | end 95 | end 96 | 97 | # api.mocki.io/v1/2ac7da28 98 | # users api.mocki.io/v1/c2037888 99 | -------------------------------------------------------------------------------- /spec/integration/abstract/commands/create_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::HTTP::Commands::Create do 4 | include_context "setup" 5 | 6 | let(:relation_class) do 7 | Class.new(ROM::HTTP::Relation) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::Integer 10 | attribute :first_name, ROM::Types::String 11 | attribute :last_name, ROM::Types::String 12 | end 13 | 14 | def by_id(id) 15 | with_query_params(id: id) 16 | end 17 | end 18 | end 19 | 20 | let(:relation) { container.relations[:users] } 21 | 22 | context "with single tuple" do 23 | let(:response) { double } 24 | 25 | let(:attributes) { {first_name: "John", last_name: "Jackson"} } 26 | 27 | let(:tuple) { attributes.merge(id: 1) } 28 | 29 | let(:command) do 30 | relation.command(:create) 31 | end 32 | 33 | let(:dataset) do 34 | ROM::HTTP::Dataset.new( 35 | uri: uri, 36 | headers: headers, 37 | request_handler: request_handler, 38 | response_handler: response_handler, 39 | base_path: :users, 40 | request_method: :post, 41 | body_params: attributes 42 | ) 43 | end 44 | 45 | before do 46 | configuration.register_relation(relation_class) 47 | 48 | allow(request_handler).to receive(:call).and_return(response) 49 | allow(response_handler).to receive(:call).and_return(tuple) 50 | end 51 | 52 | subject! { command.call(attributes) } 53 | 54 | it do 55 | is_expected.to eq(tuple) 56 | 57 | expect(request_handler).to have_received(:call).with(dataset) 58 | expect(response_handler).to have_received(:call).with(response, dataset) 59 | end 60 | end 61 | 62 | context "with a collection" do 63 | let(:response_1) { double } 64 | 65 | let(:response_2) { double } 66 | 67 | let(:attributes_1) { {first_name: "John", last_name: "Jackson"} } 68 | 69 | let(:attributes_2) { {first_name: "Jill", last_name: "Smith"} } 70 | 71 | let(:tuple_1) { attributes_1.merge(id: 1) } 72 | 73 | let(:tuple_2) { attributes_2.merge(id: 2) } 74 | 75 | let(:attributes) { [attributes_1, attributes_2] } 76 | 77 | let(:command) { relation.command(:create, result: :many) } 78 | 79 | let(:dataset_1) do 80 | ROM::HTTP::Dataset.new( 81 | uri: uri, 82 | headers: headers, 83 | request_handler: request_handler, 84 | response_handler: response_handler, 85 | base_path: :users, 86 | request_method: :post, 87 | body_params: attributes_1 88 | ) 89 | end 90 | 91 | let(:dataset_2) do 92 | ROM::HTTP::Dataset.new( 93 | uri: uri, 94 | headers: headers, 95 | request_handler: request_handler, 96 | response_handler: response_handler, 97 | base_path: :users, 98 | request_method: :post, 99 | body_params: attributes_2 100 | ) 101 | end 102 | 103 | before do 104 | configuration.register_relation(relation_class) 105 | 106 | allow(request_handler).to receive(:call).and_return(response_1, response_2) 107 | allow(response_handler).to receive(:call).and_return(tuple_1, tuple_2) 108 | end 109 | 110 | subject! { command.call(attributes) } 111 | 112 | it do 113 | is_expected.to eq([tuple_1, tuple_2]) 114 | 115 | expect(request_handler).to have_received(:call).with(dataset_1) 116 | expect(response_handler).to have_received(:call).with(response_1, dataset_1) 117 | expect(request_handler).to have_received(:call).with(dataset_2) 118 | expect(response_handler).to have_received(:call).with(response_2, dataset_2) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/integration/abstract/commands/delete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::HTTP::Commands::Delete do 4 | include_context "setup" 5 | 6 | let(:relation) do 7 | Class.new(ROM::HTTP::Relation) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::Integer 10 | end 11 | 12 | def by_id(id) 13 | with_params(id: id) 14 | end 15 | end 16 | end 17 | 18 | let(:response) { double } 19 | 20 | let(:tuple) { double } 21 | 22 | let(:tuples) { double(first: tuple) } 23 | 24 | let(:command) do 25 | Class.new(ROM::HTTP::Commands::Delete) do 26 | register_as :delete 27 | relation :users 28 | result :one 29 | end 30 | end 31 | 32 | let(:dataset) do 33 | ROM::HTTP::Dataset.new( 34 | uri: uri, 35 | headers: headers, 36 | request_handler: request_handler, 37 | response_handler: response_handler, 38 | base_path: :users, 39 | request_method: :delete 40 | ) 41 | end 42 | 43 | before do 44 | configuration.register_relation(relation) 45 | configuration.register_command(command) 46 | 47 | allow(request_handler).to receive(:call).and_return(response) 48 | allow(response_handler).to receive(:call).and_return(tuples) 49 | end 50 | 51 | subject! { container.commands[:users].delete.call } 52 | 53 | it do 54 | expect(request_handler).to have_received(:call).with(dataset) 55 | expect(response_handler).to have_received(:call).with(response, dataset) 56 | is_expected.to eq(tuple) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration/abstract/commands/update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::HTTP::Commands::Update do 4 | include_context "setup" 5 | 6 | let(:relation_class) do 7 | Class.new(ROM::HTTP::Relation) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::Integer 10 | attribute :first_name, ROM::Types::String 11 | attribute :last_name, ROM::Types::String 12 | end 13 | 14 | def by_id(id) 15 | with_query_params(id: id) 16 | end 17 | end 18 | end 19 | 20 | let(:relation) { container.relations[:users] } 21 | 22 | context "with single tuple" do 23 | let(:response) { double } 24 | 25 | let(:attributes) { {first_name: "John", last_name: "Jackson"} } 26 | 27 | let(:tuple) { attributes.merge(id: 1) } 28 | 29 | let(:command) { relation.command(:update) } 30 | 31 | let(:dataset) do 32 | ROM::HTTP::Dataset.new( 33 | uri: uri, 34 | base_path: :users, 35 | headers: headers, 36 | request_handler: request_handler, 37 | response_handler: response_handler, 38 | request_method: :put, 39 | body_params: attributes 40 | ) 41 | end 42 | 43 | before do 44 | configuration.register_relation(relation_class) 45 | 46 | allow(request_handler).to receive(:call).and_return(response) 47 | allow(response_handler).to receive(:call).and_return(tuple) 48 | end 49 | 50 | subject! { command.call(attributes) } 51 | 52 | it do 53 | is_expected.to eq(tuple) 54 | 55 | expect(request_handler).to have_received(:call).with(dataset) 56 | expect(response_handler).to have_received(:call).with(response, dataset) 57 | end 58 | end 59 | 60 | context "with a collection" do 61 | let(:response_1) { double } 62 | 63 | let(:response_2) { double } 64 | 65 | let(:attributes_1) { {first_name: "John", last_name: "Jackson"} } 66 | 67 | let(:attributes_2) { {first_name: "Jill", last_name: "Smith"} } 68 | 69 | let(:tuple_1) { attributes_1.merge(id: 1) } 70 | 71 | let(:tuple_2) { attributes_2.merge(id: 2) } 72 | 73 | let(:attributes) { [attributes_1, attributes_2] } 74 | 75 | let(:command) { relation.command(:update, result: :many) } 76 | 77 | let(:dataset_1) do 78 | ROM::HTTP::Dataset.new( 79 | uri: uri, 80 | headers: headers, 81 | request_handler: request_handler, 82 | response_handler: response_handler, 83 | base_path: :users, 84 | request_method: :put, 85 | body_params: attributes_1 86 | ) 87 | end 88 | 89 | let(:dataset_2) do 90 | ROM::HTTP::Dataset.new( 91 | uri: uri, 92 | headers: headers, 93 | request_handler: request_handler, 94 | response_handler: response_handler, 95 | base_path: :users, 96 | request_method: :put, 97 | body_params: attributes_2 98 | ) 99 | end 100 | 101 | before do 102 | configuration.register_relation(relation_class) 103 | 104 | allow(request_handler).to receive(:call).and_return(response_1, response_2) 105 | allow(response_handler).to receive(:call).and_return(tuple_1, tuple_2) 106 | end 107 | 108 | subject! { command.call(attributes) } 109 | 110 | it do 111 | expect(request_handler).to have_received(:call).with(dataset_1) 112 | expect(response_handler).to have_received(:call).with(response_1, dataset_1) 113 | expect(request_handler).to have_received(:call).with(dataset_2) 114 | expect(response_handler).to have_received(:call).with(response_2, dataset_2) 115 | is_expected.to eq([tuple_1, tuple_2]) 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/integration/abstract/relation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | require "rom-repository" 5 | 6 | RSpec.describe ROM::HTTP::Relation do 7 | subject(:users) { container.relations[:users].by_id(id).filter(query_params) } 8 | 9 | include_context "setup" 10 | 11 | let(:relation) do 12 | Class.new(ROM::HTTP::Relation) do 13 | schema(:users) do 14 | attribute :id, ROM::Types::Integer 15 | attribute :name, ROM::Types::String 16 | end 17 | 18 | def by_id(id) 19 | append_path(id.to_s) 20 | end 21 | 22 | def filter(params) 23 | with_query_params(params) 24 | end 25 | end 26 | end 27 | 28 | let(:response) { tuples.to_json } 29 | let(:tuples) { [{"id" => 1337, "name" => "John"}] } 30 | let(:id) { 1337 } 31 | let(:query_params) { {filters: {first_name: "John"}} } 32 | 33 | let(:dataset) do 34 | ROM::HTTP::Dataset.new( 35 | uri: uri, 36 | headers: headers, 37 | request_handler: request_handler, 38 | response_handler: response_handler, 39 | base_path: :users, 40 | path: id.to_s, 41 | query_params: query_params 42 | ) 43 | end 44 | 45 | before do 46 | configuration.register_relation(relation) 47 | 48 | allow(request_handler).to receive(:call).and_return(response) 49 | allow(response_handler).to receive(:call).and_return(tuples) 50 | end 51 | 52 | it "returns relation tuples" do 53 | expect(users.to_a).to eql([id: 1337, name: "John"]) 54 | 55 | expect(request_handler).to have_received(:call).with(dataset).once 56 | expect(response_handler).to have_received(:call).with(response, dataset).once 57 | end 58 | 59 | context "using a repo" do 60 | let(:repo) do 61 | Class.new(ROM::Repository) do 62 | def self.to_s 63 | "UserRepo" 64 | end 65 | end.new(container) 66 | end 67 | 68 | it "returns structs" do 69 | user = repo.users.by_id(1337).filter(query_params).first 70 | 71 | expect(user.id).to be(1337) 72 | expect(user.name).to eql("John") 73 | 74 | expect(request_handler).to have_received(:call).with(dataset).once 75 | expect(response_handler).to have_received(:call).with(response, dataset).once 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/shared/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "setup" do 4 | let(:configuration) do 5 | ROM::Configuration.new( 6 | :http, 7 | uri: uri, 8 | request_handler: request_handler, 9 | response_handler: response_handler, 10 | headers: headers 11 | ) 12 | end 13 | let(:container) { ROM.container(configuration) } 14 | let(:rom) { container } 15 | let(:gateway) { container.gateways.fetch(:default) } 16 | let(:uri) { "http://localhost:3000" } 17 | let(:request_handler) { double(Proc, freeze: self) } 18 | let(:response_handler) { double(Proc, freeze: self) } 19 | let(:headers) { {accept: "application/json"} } 20 | end 21 | -------------------------------------------------------------------------------- /spec/shared/users_and_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "users and tasks" do 4 | include_context "setup" 5 | let(:users_relation) do 6 | Class.new(ROM::HTTP::Relation) do 7 | schema(:users) do 8 | attribute :id, ROM::Types::Integer 9 | end 10 | 11 | def by_id(id) 12 | with_params(id: id) 13 | end 14 | end 15 | end 16 | let(:tasks_relation) do 17 | Class.new(ROM::HTTP::Relation) do 18 | schema(:tasks) do 19 | attribute :id, ROM::Types::Integer 20 | end 21 | 22 | def by_id(id) 23 | with_params(id: id) 24 | end 25 | end 26 | end 27 | 28 | before do 29 | configuration.register_relation(users_relation) 30 | configuration.register_relation(tasks_relation) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support/coverage" 4 | 5 | require "pathname" 6 | 7 | SPEC_ROOT = root = Pathname(__FILE__).dirname 8 | 9 | require "rom-http" 10 | require "rspec/its" 11 | require "dry/configurable/test_interface" 12 | 13 | ROM::HTTP::Dataset.enable_test_interface 14 | 15 | begin 16 | require "byebug" 17 | rescue LoadError; end 18 | 19 | require "vcr" 20 | 21 | VCR.configure do |config| 22 | config.cassette_library_dir = "#{SPEC_ROOT}/fixtures/vcr_cassettes" 23 | config.hook_into :webmock 24 | end 25 | 26 | Dir[root.join("support/**/*.rb").to_s].sort.each { |file| require file } 27 | Dir[root.join("shared/**/*.rb").to_s].sort.each { |file| require file } 28 | 29 | # Namespace holding all objects created during specs 30 | module Test 31 | def self.remove_constants 32 | constants.each(&method(:remove_const)) 33 | end 34 | end 35 | 36 | RSpec.configure do |config| 37 | config.after do 38 | Test.remove_constants 39 | end 40 | 41 | config.disable_monkey_patching! 42 | config.warnings = true 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools 4 | 5 | if ENV["COVERAGE"] == "true" 6 | require "simplecov" 7 | require "simplecov-cobertura" 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 10 | 11 | SimpleCov.start do 12 | add_filter "/spec/" 13 | enable_coverage :branch 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/mutant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Selector 5 | # Expression based test selector 6 | class Expression < self 7 | def call(_subject) 8 | integration.all_tests 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/rspec_options.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | # When no filter given, search and run focused tests 3 | config.filter_run_when_matching :focus 4 | 5 | # Disables rspec monkey patches (no reason for their existence tbh) 6 | config.disable_monkey_patching! 7 | 8 | # Run ruby in verbose mode 9 | config.warnings = true 10 | 11 | # Collect all failing expectations automatically, 12 | # without calling aggregate_failures everywhere 13 | config.define_derived_metadata do |meta| 14 | meta[:aggregate_failures] = true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | -------------------------------------------------------------------------------- /spec/unit/rom/http/dataset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::HTTP::Dataset do 4 | subject(:dataset) { ROM::HTTP::Dataset.new(**{uri: uri}.merge(options)) } 5 | 6 | let(:options) do 7 | {uri: uri} 8 | end 9 | 10 | let(:uri) { "http://localhost:3000" } 11 | 12 | describe "#uri" do 13 | context "when no uri configured" do 14 | specify do 15 | expect { ROM::HTTP::Dataset.new }.to raise_error(KeyError, /uri/) 16 | end 17 | end 18 | 19 | context "when uri configured" do 20 | context "without query_params" do 21 | let(:options) do 22 | {uri: uri} 23 | end 24 | 25 | it "returns a valid URI with a query" do 26 | expect(dataset.uri).to eql(URI(uri)) 27 | end 28 | end 29 | 30 | context "with query_params" do 31 | let(:options) do 32 | {uri: uri, query_params: {username: "John", role: "admin"}} 33 | end 34 | 35 | it "returns a valid URI with a query" do 36 | expect(dataset.uri).to eql(URI("#{uri}?username=John&role=admin")) 37 | end 38 | end 39 | 40 | context "with path" do 41 | context "without custom base_path" do 42 | let(:options) { {uri: uri, path: "/users"} } 43 | 44 | it "returns a valid URI with path appended" do 45 | expect(dataset.uri).to eql(URI("#{uri}/users")) 46 | end 47 | end 48 | 49 | context "with custom base_path" do 50 | let(:options) { {uri: uri, base_path: "/blog", path: "/users"} } 51 | 52 | it "returns a valid URI with base_path and path appended" do 53 | expect(dataset.uri).to eql(URI("#{uri}/blog/users")) 54 | end 55 | end 56 | end 57 | 58 | context "with custom base_path" do 59 | let(:options) { {uri: uri, base_path: "blog"} } 60 | 61 | it "returns a valid URI with base path appended" do 62 | expect(dataset.uri).to eql(URI("#{uri}/blog")) 63 | end 64 | end 65 | end 66 | end 67 | 68 | describe "#headers" do 69 | it "returns empty headers by default" do 70 | expect(dataset.headers).to eql({}) 71 | end 72 | 73 | context "with headers configured" do 74 | let(:headers) { {"Accept" => "application/json"} } 75 | 76 | let(:options) do 77 | {uri: uri, headers: headers} 78 | end 79 | 80 | it "returns headers" do 81 | expect(dataset.headers).to eql(headers) 82 | end 83 | end 84 | end 85 | 86 | describe "#base_path" do 87 | it "returns an empty string by default" do 88 | expect(dataset.base_path).to eql("") 89 | end 90 | 91 | context "with base_path option" do 92 | context "when base_path is absolute" do 93 | let(:options) { {uri: uri, base_path: "/users"} } 94 | 95 | it "removes the leading /" do 96 | expect(dataset.base_path).to eql("users") 97 | end 98 | end 99 | 100 | context "when base_path is not absolute" do 101 | let(:options) { {uri: uri, base_path: "users"} } 102 | 103 | it "returns provided base path" do 104 | expect(dataset.base_path).to eql("users") 105 | end 106 | end 107 | end 108 | end 109 | 110 | describe "#path" do 111 | subject { dataset.path } 112 | 113 | it "returns empty path by default" do 114 | expect(dataset.path).to eql("") 115 | end 116 | 117 | context "with base path" do 118 | let(:options) { {uri: uri, base_path: "/users"} } 119 | 120 | it "defaults to base_path" do 121 | expect(dataset.path).to eql("users") 122 | end 123 | end 124 | 125 | context "with path option" do 126 | context "when path is absolute" do 127 | let(:options) { {uri: uri, path: "/users"} } 128 | 129 | it "removes the leading /" do 130 | is_expected.to eq("users") 131 | end 132 | end 133 | 134 | context "with base path" do 135 | let(:options) { {uri: uri, base_path: "/blog", path: "/users"} } 136 | 137 | it "prepends base path" do 138 | expect(dataset.path).to eql("blog/users") 139 | end 140 | end 141 | 142 | context "when path is not absolute" do 143 | context "with base path" do 144 | let(:options) { {uri: uri, base_path: "/blog", path: "users"} } 145 | 146 | it "prepends base path" do 147 | expect(dataset.path).to eql("blog/users") 148 | end 149 | end 150 | end 151 | end 152 | end 153 | 154 | describe "#absolute_path" do 155 | it "returns default path" do 156 | expect(dataset.absolute_path).to eql("/") 157 | end 158 | 159 | context "with path option" do 160 | context "when path is absolute" do 161 | let(:options) { {uri: uri, path: "/users"} } 162 | 163 | it "returns a valid absolute path" do 164 | expect(dataset.absolute_path).to eql("/users") 165 | end 166 | end 167 | 168 | context "when path is absolute" do 169 | let(:options) { {uri: uri, path: "users"} } 170 | 171 | it "returns a valid absolute path" do 172 | expect(dataset.absolute_path).to eql("/users") 173 | end 174 | end 175 | end 176 | end 177 | 178 | describe "#get?" do 179 | it "returns true when request method is set to :get" do 180 | expect(dataset).to be_get 181 | end 182 | 183 | it "returns false when request method is not set to :get" do 184 | expect(dataset.with_request_method(:put)).to_not be_get 185 | end 186 | end 187 | 188 | describe "#post?" do 189 | it "returns true when request method is set to :post" do 190 | expect(dataset.with_request_method(:post)).to be_post 191 | end 192 | 193 | it "returns false when request method is not set to :post" do 194 | expect(dataset.with_request_method(:put)).to_not be_post 195 | end 196 | end 197 | 198 | describe "#put?" do 199 | it "returns true when request method is set to :put" do 200 | expect(dataset.with_request_method(:put)).to be_put 201 | end 202 | 203 | it "returns false when request method is not set to :put" do 204 | expect(dataset.with_request_method(:get)).to_not be_put 205 | end 206 | end 207 | 208 | describe "#delete?" do 209 | it "returns true when request method is set to :delete" do 210 | expect(dataset.with_request_method(:delete)).to be_delete 211 | end 212 | 213 | it "returns false when request method is not set to :delete" do 214 | expect(dataset.with_request_method(:get)).to_not be_delete 215 | end 216 | end 217 | 218 | describe "#request_method" do 219 | it "returns default method" do 220 | expect(dataset).to be_get 221 | end 222 | 223 | context "with request_method option" do 224 | let(:options) { {uri: uri, request_method: :put} } 225 | 226 | it "returns provided method" do 227 | expect(dataset).to be_put 228 | end 229 | end 230 | end 231 | 232 | describe "#query_params" do 233 | it "returns empty query_params by default" do 234 | expect(dataset.query_params).to eql({}) 235 | end 236 | 237 | context "with query_params option" do 238 | let(:options) { {uri: uri, query_params: {name: "Jack"}} } 239 | 240 | it "returns provided query_params" do 241 | expect(dataset.query_params).to eql(name: "Jack") 242 | end 243 | end 244 | end 245 | 246 | describe "#body_params" do 247 | it "returns empty body_params by default" do 248 | expect(dataset.body_params).to eql({}) 249 | end 250 | 251 | context "with body_params option" do 252 | let(:options) { {uri: uri, body_params: {name: "Jack"}} } 253 | 254 | it "returns provided body_params" do 255 | expect(dataset.body_params).to eql(name: "Jack") 256 | end 257 | end 258 | end 259 | 260 | describe "#with_headers" do 261 | it "returns a new dataset with provided headers" do 262 | expect(dataset.with_headers("Accept" => "application/json").headers).to eql("Accept" => "application/json") 263 | end 264 | end 265 | 266 | describe "#add_header" do 267 | let(:options) do 268 | {headers: {"Accept" => "application/json"}} 269 | end 270 | 271 | it "returns a new dataset with new headers" do 272 | expect(dataset.add_header("New", "Header").headers) 273 | .to eql("Accept" => "application/json", "New" => "Header") 274 | end 275 | end 276 | 277 | describe "#with_options" do 278 | it "returns a new dataset with new options" do 279 | expect(dataset.with_options(path: "foo").path).to eql("foo") 280 | end 281 | end 282 | 283 | describe "#with_base_path" do 284 | it "returns a new dataset with provided base_path" do 285 | expect(dataset.with_base_path("/users/tasks").base_path).to eql("users/tasks") 286 | end 287 | end 288 | 289 | describe "#with_path" do 290 | it "returns a new dataset with provided path" do 291 | expect(dataset.with_path("users").path).to eql("users") 292 | end 293 | end 294 | 295 | describe "#append_path" do 296 | context "with base_path" do 297 | let(:options) { {base_path: "/users"} } 298 | 299 | it "returns a new dataset with provided path appended to previous path" do 300 | expect(dataset.append_path("tasks").path).to eql("users/tasks") 301 | end 302 | end 303 | 304 | context "without existing path" do 305 | it "returns a new dataset with provided path" do 306 | expect(dataset.append_path("users").path).to eql("users") 307 | end 308 | end 309 | 310 | context "with existing path" do 311 | let(:options) { {path: "/users"} } 312 | 313 | it "returns a new dataset with provided path appended to previous path" do 314 | expect(dataset.append_path("tasks").path).to eql("users/tasks") 315 | end 316 | end 317 | end 318 | 319 | describe "#with_request_method" do 320 | it "returns a new dataset with provided request method" do 321 | expect(dataset.with_request_method(:put).request_method).to be(:put) 322 | end 323 | end 324 | 325 | describe "#with_query_params" do 326 | it "returns a new dataset with new query_params" do 327 | expect(dataset.with_query_params(admin: true).query_params).to eql(admin: true) 328 | end 329 | end 330 | 331 | describe "#add_query_params" do 332 | let(:options) do 333 | {query_params: {age: 21}} 334 | end 335 | 336 | it "returns a new dataset with query_params appended" do 337 | expect(dataset.add_query_params(admin: true).query_params).to eql(age: 21, admin: true) 338 | end 339 | end 340 | 341 | describe "#with_body_params" do 342 | it "returns a new dataset with new body_params" do 343 | expect(dataset.with_body_params(admin: true).body_params).to eql(admin: true) 344 | end 345 | end 346 | 347 | describe "#add_body_params" do 348 | let(:options) do 349 | {body_params: {age: 21}} 350 | end 351 | 352 | it "returns a new dataset with body_params appended" do 353 | expect(dataset.add_body_params(admin: true).body_params).to eql(age: 21, admin: true) 354 | end 355 | end 356 | 357 | describe "#each" do 358 | let(:response) { double(Array) } 359 | let(:block) { proc {} } 360 | let(:result) { double } 361 | 362 | before do 363 | allow(dataset).to receive(:response).and_return(response) 364 | allow(response).to receive(:each).and_yield.and_return(result) 365 | end 366 | 367 | context "with no block given" do 368 | subject! { dataset.each } 369 | 370 | it { expect(dataset).to_not have_received(:response) } 371 | it { expect(response).to_not have_received(:each) } 372 | it { is_expected.to be_kind_of(Enumerable) } 373 | end 374 | 375 | context "with block given" do 376 | subject! { dataset.each(&block) } 377 | 378 | it { expect(dataset).to have_received(:response).once } 379 | it { expect(response).to have_received(:each) } 380 | it { is_expected.to eq(result) } 381 | end 382 | end 383 | 384 | describe "#insert" do 385 | let(:name) { "Jill" } 386 | let(:attributes) { {user: {name: name}} } 387 | let(:new_dataset) { double(ROM::HTTP::Dataset) } 388 | let(:response) { double } 389 | 390 | before do 391 | allow(dataset).to receive(:with_options).and_return(new_dataset) 392 | allow(new_dataset).to receive(:response).and_return(response) 393 | end 394 | 395 | subject! { dataset.insert(attributes) } 396 | 397 | it do 398 | expect(dataset).to have_received(:with_options).with( 399 | request_method: :post, 400 | body_params: attributes 401 | ) 402 | end 403 | it { expect(new_dataset).to have_received(:response) } 404 | it { is_expected.to eq(response) } 405 | end 406 | 407 | describe "#update" do 408 | let(:name) { "Jill" } 409 | let(:attributes) { {user: {name: name}} } 410 | let(:new_dataset) { double(ROM::HTTP::Dataset) } 411 | let(:response) { double } 412 | 413 | before do 414 | allow(dataset).to receive(:with_options).and_return(new_dataset) 415 | allow(new_dataset).to receive(:response).and_return(response) 416 | end 417 | 418 | subject! { dataset.update(attributes) } 419 | 420 | it do 421 | expect(dataset).to have_received(:with_options).with( 422 | request_method: :put, 423 | body_params: attributes 424 | ) 425 | end 426 | it { expect(new_dataset).to have_received(:response) } 427 | it { is_expected.to eq(response) } 428 | end 429 | 430 | describe "#delete" do 431 | let(:new_dataset) { double(ROM::HTTP::Dataset) } 432 | let(:response) { double } 433 | 434 | before do 435 | allow(dataset).to receive(:with_options).and_return(new_dataset) 436 | allow(new_dataset).to receive(:response).and_return(response) 437 | end 438 | 439 | subject! { dataset.delete } 440 | 441 | it do 442 | expect(dataset).to have_received(:with_options).with( 443 | request_method: :delete 444 | ) 445 | end 446 | it { expect(new_dataset).to have_received(:response) } 447 | it { is_expected.to eq(response) } 448 | end 449 | 450 | describe "#response" do 451 | let(:options) do 452 | {request_handler: request_handler, 453 | response_handler: response_handler, 454 | path: "test", 455 | query_params: {ok: true}} 456 | end 457 | 458 | let(:request_handler) do 459 | -> (ds) { ds.query_params } 460 | end 461 | 462 | let(:response_handler) do 463 | -> (response, ds) { [response[:ok], ds.path] } 464 | end 465 | 466 | it "issues a request via request handler and handles response via response handler" do 467 | expect(dataset.response).to eql([true, "test"]) 468 | end 469 | end 470 | end 471 | -------------------------------------------------------------------------------- /spec/unit/rom/http/gateway_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/lint/spec" 4 | 5 | RSpec.describe ROM::HTTP::Gateway do 6 | include_context "users and tasks" 7 | 8 | it_behaves_like "a rom gateway" do 9 | let(:identifier) { :http } 10 | 11 | let(:gateway) { ROM::HTTP::Gateway } 12 | 13 | let(:options) do 14 | { 15 | uri: "http://localhost:3000", 16 | request_handler: request_handler, 17 | response_handler: response_handler 18 | } 19 | end 20 | end 21 | 22 | describe "#dataset?" do 23 | it "returns true if a table exists" do 24 | expect(gateway.dataset?(:users)).to be(true) 25 | end 26 | 27 | it "returns false if a table does not exist" do 28 | expect(gateway.dataset?(:not_here)).to be(false) 29 | end 30 | end 31 | 32 | describe "#dataset" do 33 | context "when extended" do 34 | subject(:gateway) { Test::Gateway.new(uri: "test", **config) } 35 | 36 | let(:config) { {} } 37 | 38 | before do 39 | module Test 40 | class Gateway < ROM::HTTP::Gateway; end 41 | end 42 | end 43 | 44 | context "when no Dataset defined in the same namespace" do 45 | it "returns ROM::HTTP::Dataset" do 46 | expect(gateway.dataset(:test)).to be_instance_of(ROM::HTTP::Dataset) 47 | end 48 | end 49 | 50 | context "when Dataset defined in the same namespace" do 51 | before do 52 | module Test 53 | class Dataset < ROM::HTTP::Dataset; end 54 | end 55 | end 56 | 57 | it "returns ROM::HTTP::Dataset" do 58 | expect(gateway.dataset(:test)).to be_instance_of(Test::Dataset) 59 | end 60 | end 61 | 62 | context "when handlers identifier is configured" do 63 | let(:config) do 64 | {handlers: :json} 65 | end 66 | 67 | let(:dataset) do 68 | gateway.dataset(:test) 69 | end 70 | 71 | it "sets registered request handler" do 72 | expect(dataset.request_handler).to be(ROM::HTTP::Handlers[:json][:request]) 73 | end 74 | 75 | it "sets registered response handler" do 76 | expect(dataset.response_handler).to be(ROM::HTTP::Handlers[:json][:response]) 77 | end 78 | end 79 | end 80 | 81 | context "when not extended" do 82 | subject(:gateway) { ROM::HTTP::Gateway.new(uri: "test") } 83 | 84 | it "returns ROM::HTTP::Dataset" do 85 | expect(gateway.dataset(:test)).to be_instance_of(ROM::HTTP::Dataset) 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/unit/rom/http/handlers/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/http/handlers/json" 4 | 5 | RSpec.describe ROM::HTTP::Handlers do 6 | describe "JSON" do 7 | subject(:dataset) do 8 | ROM::HTTP::Dataset.new( 9 | uri: uri, 10 | request_handler: ROM::HTTP::Handlers::JSONRequest, 11 | response_handler: ROM::HTTP::Handlers::JSONResponse 12 | ) 13 | end 14 | 15 | let(:uri) do 16 | "https://api.github.com" 17 | end 18 | 19 | it "loads an array with hashes from the response body" do 20 | VCR.use_cassette(:github_repos) do 21 | org = dataset.with_path("/orgs/rom-rb").first 22 | 23 | expect(org["id"]).to be(4_589_832) 24 | expect(org["login"]).to eql("rom-rb") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/rom/http/relation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::HTTP::Relation do 4 | subject(:relation) { relation_klass.new(dataset) } 5 | 6 | let(:relation_klass) do 7 | Class.new(ROM::HTTP::Relation) do 8 | schema do 9 | attribute :id, ROM::Types::Integer.meta(primary_key: true) 10 | attribute :name, ROM::Types::String 11 | end 12 | end 13 | end 14 | 15 | let(:dataset) { instance_double(ROM::HTTP::Dataset, response: data, map: data) } 16 | 17 | let(:data) do 18 | [{id: 1, name: "John"}, {id: 2, name: "Jill"}] 19 | end 20 | 21 | describe "#primary_key" do 22 | before do 23 | relation.schema.finalize_attributes! 24 | end 25 | 26 | it "returns configured primary key name" do 27 | expect(relation.primary_key).to be(:id) 28 | end 29 | 30 | it "returns nil when primary key was not defined" do 31 | relation = Class.new(ROM::HTTP::Relation) { schema {} }.new([]) 32 | expect(relation.primary_key).to be(nil) 33 | end 34 | end 35 | 36 | describe "#project" do 37 | it "returns the projected data" do 38 | expect(relation.project(:id).to_a).to eql([{id: 1}, {id: 2}]) 39 | end 40 | end 41 | 42 | describe "#exclude" do 43 | subject { relation.exclude(:id).to_a } 44 | 45 | it "returns the data with specified keys excluded" do 46 | expect(relation.exclude(:id).to_a).to eql([{name: "John"}, {name: "Jill"}]) 47 | end 48 | end 49 | 50 | describe "#rename" do 51 | subject { relation.rename(id: :identity).to_a } 52 | 53 | it "returns the data with keys renamed according to mapping" do 54 | expect(relation.rename(id: :identity).to_a) 55 | .to eql([{name: "John", identity: 1}, {name: "Jill", identity: 2}]) 56 | end 57 | end 58 | 59 | describe "#prefix" do 60 | it "returns the data with prefixed keys" do 61 | expect(relation.prefix("user").to_a) 62 | .to match_array([{user_id: 1, user_name: "John"}, {user_id: 2, user_name: "Jill"}]) 63 | end 64 | end 65 | 66 | describe "#to_a" do 67 | context "with standard schema" do 68 | let(:relation_klass) do 69 | Class.new(ROM::HTTP::Relation) do 70 | schema do 71 | attribute :id, ROM::Types::Strict::Integer 72 | end 73 | end 74 | end 75 | 76 | it "applies the schema and returns the materialized results" do 77 | expect(relation.to_a).to eql([{id: 1}, {id: 2}]) 78 | end 79 | end 80 | 81 | context "with aliased schema" do 82 | let(:relation_klass) do 83 | Class.new(ROM::HTTP::Relation) do 84 | schema do 85 | attribute :id, ROM::Types::Strict::Integer 86 | attribute :name, ROM::Types::Strict::String, alias: :username 87 | end 88 | end 89 | end 90 | 91 | it "applies the schema and returns the materialized results" do 92 | expect(relation.to_a).to eql([{id: 1, username: "John"}, {id: 2, username: "Jill"}]) 93 | end 94 | end 95 | end 96 | 97 | describe "#insert" do 98 | let(:result) do 99 | relation.insert(body_params: {name: "John"}) 100 | end 101 | 102 | context "with single tuple" do 103 | let(:data) { {id: 1, name: "John"} } 104 | 105 | it "applies the schema and returns the materialized results" do 106 | expect(dataset).to receive(:insert).and_return(data) 107 | expect(result).to eql(id: 1, name: "John") 108 | end 109 | end 110 | 111 | context "with many tuples" do 112 | let(:data) do 113 | [{id: 1, name: "John"}, {id: 2, name: "Jill"}] 114 | end 115 | 116 | it "applies the schema and returns the materialized results" do 117 | expect(dataset).to receive(:insert).and_return(data) 118 | expect(result).to eql([{id: 1, name: "John"}, {id: 2, name: "Jill"}]) 119 | end 120 | end 121 | end 122 | 123 | describe "#update" do 124 | let(:result) do 125 | relation.update(body_params: {name: "John"}) 126 | end 127 | 128 | context "with single tuple" do 129 | let(:data) { {id: 1, name: "John"} } 130 | 131 | it "applies the schema and returns the materialized results" do 132 | expect(dataset).to receive(:update).and_return(data) 133 | expect(result).to eql(id: 1, name: "John") 134 | end 135 | end 136 | 137 | context "with many tuples" do 138 | let(:data) do 139 | [{id: 1, name: "John"}, {id: 2, name: "Jill"}] 140 | end 141 | 142 | it "applies the schema and returns the materialized results" do 143 | expect(dataset).to receive(:update).and_return(data) 144 | expect(result).to eql([{id: 1, name: "John"}, {id: 2, name: "Jill"}]) 145 | end 146 | end 147 | end 148 | 149 | describe "#delete" do 150 | let(:result) do 151 | relation.delete 152 | end 153 | 154 | it "forwards to its dataset" do 155 | expect(dataset).to receive(:delete).and_return(data) 156 | expect(relation.delete).to eql(data) 157 | end 158 | end 159 | end 160 | --------------------------------------------------------------------------------