whiplash-app 0.9.2 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -98
- data/lib/whiplash/app/api_config.rb +6 -4
- data/lib/whiplash/app/connections.rb +1 -1
- data/lib/whiplash/app/controller_helpers.rb +80 -0
- data/lib/whiplash/app/railtie.rb +19 -2
- data/lib/whiplash/app/version.rb +1 -1
- data/lib/whiplash/app.rb +18 -10
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0e2ae334bdd40de1ae96543cd86eb7506e3de9a7d65d04b003fa67d2eb83da1
|
4
|
+
data.tar.gz: a2ae0fe3f75c4ed25e7703c85f71495609f10434c9bf3d687483d1ad440a5513
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fd9e63cc8d1359a38adc462eb4818928d4ea84bc1e57db46e1fa9725ef547c59172763727f8fc7957c8de22d0a3720a437040789966116ce5a1c0a833ad210f
|
7
|
+
data.tar.gz: 0f35b12647a50d4a4c8fbb1a0f24fb6d40946c19dbbeb4d0238eb19d5c075fd3958e2cea68a72072d8db5770c83d18a5c1bc831a3bee2497e9cc7a4bfde52913
|
data/README.md
CHANGED
@@ -4,6 +4,8 @@ The whiplash-app gem allows your Whiplash application to access the Whiplash
|
|
4
4
|
API and perform authentication, signatures and signature verification, and basic
|
5
5
|
CRUD functions against the api.
|
6
6
|
|
7
|
+
For apps that provide a UI, it also provides built in authentication and several helper methods.
|
8
|
+
|
7
9
|
## Installation
|
8
10
|
|
9
11
|
Add this line to your application's Gemfile:
|
@@ -22,109 +24,68 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
## Usage
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
1. `Whiplash::App` must now be instantiated.
|
29
|
-
2. Tokens are **not** automatically refreshed
|
27
|
+
There are two basic uses for this gem:
|
28
|
+
1. Authenticating users for apps _with a UI_ (i.e. Notifications, Troubleshoot, etc)
|
29
|
+
2. Providing offline access to applications that perform tasks (i.e Tasks, Old Integrations, etc)
|
30
30
|
|
31
|
-
|
32
|
-
```ruby
|
33
|
-
api = Whiplash::App
|
34
|
-
```
|
31
|
+
It's not uncommon for an application to do _both_ of the above (i.e. Notifications, Payments, etc)
|
35
32
|
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
api = Whiplash::App.new
|
39
|
-
api.refresh_token! # Since you don't have one yet
|
40
|
-
api.token # Confirm you've got a token
|
41
|
-
. . .
|
42
|
-
api.refresh_token! if api.token_expired?
|
43
|
-
```
|
44
|
-
|
45
|
-
### Authentication
|
33
|
+
### Authentication for offline access (Oauth Client Credentials flow)
|
46
34
|
In order to authenticate, make sure the following `ENV` vars are set:
|
47
35
|
|
48
36
|
```ruby
|
49
|
-
ENV[
|
50
|
-
ENV[
|
51
|
-
ENV[
|
37
|
+
ENV['WHIPLASH_API_URL']
|
38
|
+
ENV['WHIPLASH_CLIENT_ID']
|
39
|
+
ENV['WHIPLASH_CLIENT_SCOPE']
|
40
|
+
ENV['WHIPLASH_CLIENT_SECRET']
|
52
41
|
```
|
53
42
|
|
54
|
-
Once those are set,
|
55
|
-
|
56
|
-
### Oauth Client Credentials
|
57
|
-
You can authenticate using Oauth Client Credentials (i.e. auth an entire app).
|
58
|
-
You probably want this for apps that work offline, _on behalf_ of users or customers, or that don't work at the user/customer-level at all.
|
43
|
+
Once those are set, you can generate and use an access token like so:
|
59
44
|
|
60
45
|
```ruby
|
61
|
-
|
62
|
-
api
|
63
|
-
api.
|
46
|
+
token = Whiplash::App.client_credentials_token
|
47
|
+
api = Whiplash::App.new(token)
|
48
|
+
customers = api.get!('customers')
|
64
49
|
```
|
65
50
|
|
66
|
-
###
|
67
|
-
|
51
|
+
### Authentication for online access
|
52
|
+
In order to use the API, you only need to set the following:
|
68
53
|
|
69
54
|
```ruby
|
70
|
-
|
71
|
-
api = Whiplash::App.new(oauth_credentials_hash)
|
72
|
-
api.token # Confirm you've got a token
|
55
|
+
ENV['WHIPLASH_API_URL']
|
73
56
|
```
|
74
57
|
|
75
|
-
|
76
|
-
In order to set your api url, you can use the following environment URL:
|
77
|
-
```
|
78
|
-
ENV["WHIPLASH_API_URL"]
|
79
|
-
```
|
80
|
-
If it isn't set, then the API URL defaults to either `https://sandbox.getwhiplash.com` (test or dev environment) or `https://www.getwhiplash.com` (prod environment).
|
58
|
+
As long as all of your apps are on the same subdomain, they will share auth cookies:
|
81
59
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
In order to make the use of the gem seem more "AR-ish", we've added AR oriented methods that can be used for basic object creation/deletion/updating/viewing. The basic gist of these AR style CRUD methods is that they will all follow the same pattern. If you are performing a collection action, such as `create` or `find`, the pattern is this:
|
89
|
-
|
90
|
-
```ruby
|
91
|
-
api.create(resource, params, headers)
|
60
|
+
```json
|
61
|
+
{
|
62
|
+
"oauth_token": XXXXXXX,
|
63
|
+
"user": {"id":151,"email":"mark@getwhiplash.com","role":"admin","locale":"en","first_name":"Mark","last_name":"Dickson","partner_id":null,"warehouse_id": 1,"customer_ids":[1, 2, 3]}
|
64
|
+
}
|
92
65
|
```
|
93
66
|
|
94
|
-
|
67
|
+
You get a variety of helper methods for free:
|
95
68
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
69
|
+
`init_whiplash_api` - This instantiates `@whiplash_api` which can be used to make requests, out of the box
|
70
|
+
`current_user` - This is a **hash** with the above fields; you typically shouldn't need much more user info than this
|
71
|
+
`require_user` - Typically you'd use this in a `before_action`. You almost always want this in `ApplicationController`.
|
72
|
+
`set_locale!` - Sets the locale based on the value in the user hash
|
73
|
+
`set_current_user_cookie!` - Updates the current user cookie with fresh data from the api. You typically won't need this, unless your app updates fields like `warehouse_id` or `locale`.
|
74
|
+
`core_url` - Shorthand for `ENV['WHIPLASH_API_URL']`
|
75
|
+
`core_url_for` - Link back to Core like `core_url_for('login')`
|
102
76
|
|
103
|
-
```ruby
|
104
|
-
api.update(resource, id, params_to_update, headers)
|
105
|
-
```
|
106
77
|
|
107
|
-
|
78
|
+
### Sending Customer ID and Shop ID headers
|
79
|
+
You can send the headers in `headers` array, like `{customer_id: 123, shop_id: 111}`.
|
80
|
+
Alternatively, you can set them on instantiation like `Whiplash::App.new(token, {customer_id: 123, shop_id: 111})`
|
108
81
|
|
109
|
-
```ruby
|
110
|
-
api.find_all('orders', {}, { customer_id: 187 })
|
111
|
-
api.find('orders', 1)
|
112
|
-
api.create('orders', { key: "value", key2: "value" }, { customer_id: 187 } )
|
113
|
-
api.update('orders', 1, { key: "value"}, { customer_id: 187 } )
|
114
|
-
api.destroy('orders', 1, { customer_id: 187 } )
|
115
|
-
api.count('customers')
|
116
|
-
```
|
117
82
|
|
118
83
|
### CRUD Wrapper methods
|
119
|
-
In reality, all of these methods are simply wrapper methods around simple `GET/POST/PUT/DELETE` wrappers on Faraday, so if you want to get more granular,you can also make calls that simply reference the lower level REST verb:
|
120
84
|
|
121
85
|
```ruby
|
122
86
|
api.get('orders')
|
123
87
|
```
|
124
|
-
Which will return all orders and roughly correspond to an index call. If you need to use `Whiplash::App` for nonRESTful calls, simply drop the full endpoint in as your first argument:
|
125
88
|
|
126
|
-
```ruby
|
127
|
-
api.get('orders/non_restful_action', {}, {})
|
128
89
|
```
|
129
90
|
`POST`, `PUT`, and `DELETE` calls can be performed in much the same way:
|
130
91
|
```ruby
|
@@ -133,6 +94,37 @@ api.put(endpoint, params, headers) # PUT request to the specified endpoint passi
|
|
133
94
|
api.delete(endpoint, params, headers) # DELETE request to the specified endpoint. Params would probably just be an id.
|
134
95
|
```
|
135
96
|
|
97
|
+
### Bang methods
|
98
|
+
|
99
|
+
In typical Rails/Ruby fashion, `!` methods `raise`. Typically, you'll want to set some global `rescue`s and use the `!` version of crud requests:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
rescue_from WhiplashApiError, with: :handle_whiplash_api_error
|
103
|
+
|
104
|
+
def handle_whiplash_api_error(exception)
|
105
|
+
# Any special exceptions we want to handle directly
|
106
|
+
case exception.class.to_s
|
107
|
+
when 'WhiplashApiError::Unauthorized'
|
108
|
+
return redirect_to core_url_for('logout')
|
109
|
+
end
|
110
|
+
|
111
|
+
@status_code = WhiplashApiError.codes&.invert&.dig(exception&.class)
|
112
|
+
@error = exception.message
|
113
|
+
respond_to do |format|
|
114
|
+
format.html {
|
115
|
+
flash[:error] = @error
|
116
|
+
redirect_back(fallback_location: root_path)
|
117
|
+
}
|
118
|
+
format.json {
|
119
|
+
render json: exception, status: @status_code
|
120
|
+
}
|
121
|
+
format.js {
|
122
|
+
render template: 'resources/exception'
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
136
128
|
### Signing and Verifying.
|
137
129
|
`whiplash-app` supports signing and verifying signatures like so:
|
138
130
|
```ruby
|
@@ -143,30 +135,6 @@ and verifications are done like so:
|
|
143
135
|
Whiplash::App.verified?(request)
|
144
136
|
```
|
145
137
|
|
146
|
-
### Caching
|
147
|
-
`whiplash-app` is Cache agnostic, relying on the `moneta` gem to provide a local store, if needed.
|
148
|
-
However, if you intend to specify `REDIS` as your key-value store of choice, it's dead simple. Simply declare the following variables:
|
149
|
-
```
|
150
|
-
ENV["REDIS_HOST"]
|
151
|
-
ENV["REDIS_PORT"]
|
152
|
-
ENV["REDIS_PASSWORD"]
|
153
|
-
ENV["REDIS_NAMESPACE"]
|
154
|
-
```
|
155
|
-
If those are provided, `moneta` will use your redis connection and will namespace your cache storage under the redis namespace. By default, if you do not declare a `REDIS_NAMESPACE` value, the app will default to the `WHIPLASH_CLIENT_ID`.
|
156
|
-
|
157
|
-
**For user-facing apps, best practice is to store the `oauth_credentials_hash` in a session variable.**
|
158
|
-
|
159
|
-
### Gotchas
|
160
|
-
Due to the way Faraday handles params, this would not, as expected, route to `orders#show` in the Whiplash App, but would instead route to `orders#index`, so it wouldn't return the expected singular order with an ID of 1, but all orders for that customer.
|
161
|
-
```ruby
|
162
|
-
api.get('orders', {id: 1}, {customer_id: 187})
|
163
|
-
```
|
164
|
-
Instead, you'd want to do:
|
165
|
-
```ruby
|
166
|
-
api.get('orders/1', {}, {customer_id: 187})
|
167
|
-
```
|
168
|
-
|
169
|
-
|
170
138
|
## Development
|
171
139
|
|
172
140
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -10,6 +10,12 @@ module Whiplash
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
+
def rate_limit
|
14
|
+
(ENV['WHIPLASH_RATE_LIMIT'] || 25).to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
13
19
|
def production_url
|
14
20
|
ENV["WHIPLASH_API_URL"] || "https://www.getwhiplash.com"
|
15
21
|
end
|
@@ -18,10 +24,6 @@ module Whiplash
|
|
18
24
|
ENV["WHIPLASH_API_URL"] || "https://sandbox.getwhiplash.com"
|
19
25
|
end
|
20
26
|
|
21
|
-
def rate_limit
|
22
|
-
(ENV['WHIPLASH_RATE_LIMIT'] || 25).to_i
|
23
|
-
end
|
24
|
-
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
@@ -27,7 +27,7 @@ module Whiplash
|
|
27
27
|
|
28
28
|
def app_request(options={})
|
29
29
|
return base_app_request(options) unless defined?(Sidekiq)
|
30
|
-
limiter = Sidekiq::Limiter.window('whiplash-core', self.rate_limit, :second, wait_timeout: 15)
|
30
|
+
limiter = Sidekiq::Limiter.window('whiplash-core', self.class.rate_limit, :second, wait_timeout: 15)
|
31
31
|
limiter.within_limit do
|
32
32
|
base_app_request(options)
|
33
33
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Whiplash
|
3
|
+
class App
|
4
|
+
module ControllerHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
helper_method :cookie_domain,
|
9
|
+
:core_url,
|
10
|
+
:core_url_for,
|
11
|
+
:current_user
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def cookie_domain
|
17
|
+
'.' + URI.parse(core_url).host
|
18
|
+
end
|
19
|
+
|
20
|
+
def core_url
|
21
|
+
ENV['WHIPLASH_API_URL']
|
22
|
+
end
|
23
|
+
|
24
|
+
def core_url_for(path)
|
25
|
+
[core_url, path].join('/')
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_user
|
29
|
+
return if cookies[:user].blank?
|
30
|
+
begin
|
31
|
+
@current_user ||= JSON.parse(cookies[:user])
|
32
|
+
rescue StandardError => e
|
33
|
+
Rails.logger.warn "User could not be initialized: #{e.message}"
|
34
|
+
@current_user = nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def http_scheme
|
39
|
+
URI(core_url).scheme
|
40
|
+
end
|
41
|
+
|
42
|
+
def init_whiplash_api(options = {})
|
43
|
+
return redirect_to core_url_for('login') if cookies[:oauth_token].blank?
|
44
|
+
token = {access_token: cookies[:oauth_token]}
|
45
|
+
begin
|
46
|
+
@whiplash_api = Whiplash::App.new(token, options)
|
47
|
+
rescue StandardError => e
|
48
|
+
Rails.logger.warn "API failed to initialize: #{e.message}"
|
49
|
+
@whiplash_api = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def require_user
|
54
|
+
redirect_to core_url_for('login') if current_user.blank?
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_locale!
|
58
|
+
I18n.default_locale = :en
|
59
|
+
I18n.locale = current_user.try('locale') || I18n.default_locale
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def set_current_user_cookie!(expires_at = nil)
|
64
|
+
user = @whiplash_api.get!("me").body
|
65
|
+
fields_we_care_about = %w(id email role locale first_name last_name partner_id warehouse_id customer_ids)
|
66
|
+
user_hash = user.slice(*fields_we_care_about)
|
67
|
+
expires_at ||= user['current_sign_in_expires_at']
|
68
|
+
|
69
|
+
shared_values = {
|
70
|
+
expires: DateTime.parse(expires_at),
|
71
|
+
secure: http_scheme == 'https',
|
72
|
+
samesite: :strict,
|
73
|
+
domain: cookie_domain
|
74
|
+
}
|
75
|
+
cookies[:user] = shared_values.merge(value: user_hash.to_json)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/whiplash/app/railtie.rb
CHANGED
@@ -1,14 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Whiplash
|
4
|
-
|
4
|
+
class App
|
5
5
|
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
config.before_configuration do |app|
|
8
|
+
# App name/etc, mainly for consistency in logging
|
9
|
+
app_name = app.class.module_parent.name.underscore.dasherize
|
10
|
+
app.config.environment_key = ENV.fetch('ENVIRONMENT_KEY', Rails.env.to_s)
|
11
|
+
app.config.application_key = ENV.fetch('APPLICATION_KEY', app_name)
|
12
|
+
app.config.application_name_space = [config.application_key, config.environment_key].join('-')
|
13
|
+
|
14
|
+
# session settings
|
15
|
+
session_days = 30
|
16
|
+
session_seconds = session_days * 24 * 60 * 60
|
17
|
+
session_length = ENV.fetch('SESSION_LENGTH', session_seconds).to_i
|
18
|
+
app.config.session_length = session_length
|
19
|
+
app.config.session_store :cookie_store, :key => '_session', :expire_after => session_length
|
20
|
+
end
|
21
|
+
|
6
22
|
initializer "whiplash_app.action_controller" do
|
7
23
|
ActiveSupport.on_load(:action_controller) do
|
8
|
-
puts "Extending #{self} with YourGemsModuleName::Controller"
|
9
24
|
include Whiplash::App::CanonicalHost
|
25
|
+
include Whiplash::App::ControllerHelpers
|
10
26
|
end
|
11
27
|
end
|
28
|
+
|
12
29
|
end
|
13
30
|
end
|
14
31
|
end
|
data/lib/whiplash/app/version.rb
CHANGED
data/lib/whiplash/app.rb
CHANGED
@@ -9,16 +9,17 @@ require "faraday"
|
|
9
9
|
|
10
10
|
# Rails app stuff
|
11
11
|
if defined?(Rails::Railtie)
|
12
|
-
require "whiplash/app/canonical_host"
|
13
12
|
require "whiplash/app/railtie"
|
13
|
+
require "whiplash/app/canonical_host"
|
14
|
+
require "whiplash/app/controller_helpers"
|
14
15
|
end
|
15
16
|
|
16
17
|
module Whiplash
|
17
18
|
class App
|
18
|
-
|
19
|
+
extend Whiplash::App::Signing
|
20
|
+
extend Whiplash::App::ApiConfig
|
19
21
|
include Whiplash::App::Connections
|
20
22
|
include Whiplash::App::FinderMethods
|
21
|
-
extend Whiplash::App::Signing
|
22
23
|
|
23
24
|
attr_accessor :customer_id, :shop_id, :token
|
24
25
|
|
@@ -29,16 +30,16 @@ module Whiplash
|
|
29
30
|
@api_version = options[:api_version] || 2 # can be 2_1
|
30
31
|
end
|
31
32
|
|
32
|
-
def client
|
33
|
-
OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: api_url)
|
34
|
-
end
|
35
|
-
|
36
33
|
def versioned_api_url
|
37
34
|
"api/v#{@api_version}"
|
38
35
|
end
|
39
36
|
|
37
|
+
def client
|
38
|
+
OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: self.class.api_url)
|
39
|
+
end
|
40
|
+
|
40
41
|
def connection
|
41
|
-
Faraday.new [api_url, versioned_api_url].join("/") do |conn|
|
42
|
+
Faraday.new [self.class.api_url, versioned_api_url].join("/") do |conn|
|
42
43
|
conn.request :authorization, 'Bearer', token.token
|
43
44
|
conn.request :json
|
44
45
|
conn.response :json, :content_type => /\bjson$/
|
@@ -53,9 +54,9 @@ module Whiplash
|
|
53
54
|
case ENV["WHIPLASH_CLIENT_SCOPE"]
|
54
55
|
when /app_(manage|read)/
|
55
56
|
begin
|
56
|
-
access_token =
|
57
|
+
access_token = self.class.client_credentials_token
|
57
58
|
rescue URI::InvalidURIError => e
|
58
|
-
raise StandardError, "The
|
59
|
+
raise StandardError, "The provided URL (#{ENV["WHIPLASH_API_URL"]}) is not valid"
|
59
60
|
end
|
60
61
|
else
|
61
62
|
raise StandardError, "You must request an access token before you can refresh it" if token.nil?
|
@@ -70,6 +71,13 @@ module Whiplash
|
|
70
71
|
false
|
71
72
|
end
|
72
73
|
|
74
|
+
class << self
|
75
|
+
def client_credentials_token
|
76
|
+
client = OAuth2::Client.new(ENV["WHIPLASH_CLIENT_ID"], ENV["WHIPLASH_CLIENT_SECRET"], site: api_url)
|
77
|
+
client.client_credentials.get_token(scope: ENV["WHIPLASH_CLIENT_SCOPE"])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
73
81
|
private
|
74
82
|
def format_token(oauth_token)
|
75
83
|
return oauth_token if oauth_token.is_a?(OAuth2::AccessToken)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whiplash-app
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Don Sullivan, Mark Dickson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oauth2
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- lib/whiplash/app/api_config.rb
|
118
118
|
- lib/whiplash/app/canonical_host.rb
|
119
119
|
- lib/whiplash/app/connections.rb
|
120
|
+
- lib/whiplash/app/controller_helpers.rb
|
120
121
|
- lib/whiplash/app/finder_methods.rb
|
121
122
|
- lib/whiplash/app/railtie.rb
|
122
123
|
- lib/whiplash/app/signing.rb
|