lhs 19.8.2 → 20.1.2
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 +89 -5
- data/lhs.gemspec +2 -0
- data/lib/lhs.rb +18 -0
- data/lib/lhs/concerns/option_blocks.rb +2 -1
- data/lib/lhs/concerns/record/href_for.rb +19 -0
- data/lib/lhs/concerns/record/request.rb +4 -4
- data/lib/lhs/interceptors/extended_rollbar/handler.rb +40 -0
- data/lib/lhs/interceptors/extended_rollbar/interceptor.rb +20 -0
- data/lib/lhs/interceptors/extended_rollbar/thread_registry.rb +19 -0
- data/lib/lhs/interceptors/request_cycle_cache/interceptor.rb +41 -0
- data/lib/lhs/interceptors/request_cycle_cache/thread_registry.rb +18 -0
- data/lib/lhs/railtie.rb +8 -1
- data/lib/lhs/record.rb +3 -8
- data/lib/lhs/rspec.rb +19 -0
- data/lib/lhs/test/stub.rb +29 -0
- data/lib/lhs/version.rb +1 -1
- data/spec/dummy/app/controllers/extended_rollbar_controller.rb +10 -0
- data/spec/dummy/config/initializers/rollbar.rb +9 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/extended_rollbar_spec.rb +67 -0
- data/spec/rails_helper.rb +2 -0
- data/spec/record/href_for_spec.rb +25 -0
- data/spec/request_cycle_cache_spec.rb +4 -4
- data/spec/stubs/all_spec.rb +28 -0
- metadata +49 -7
- data/lib/lhs/concerns/record/request_cycle_cache/interceptor.rb +0 -40
- data/lib/lhs/concerns/record/request_cycle_cache/request_cycle_thread_registry.rb +0 -15
- data/lib/lhs/test/request_cycle_cache_helper.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6222738010dc108833f001419f0fe23f97c1fbe6bb3211347724455bd6d1fab6
|
4
|
+
data.tar.gz: ec1856d1a8ce39396dedf8251058f59604f3e84111a2dffea05a7b0d97b07cab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22dde92abf63322ceab6d51962462c5dac3a34c12c46438a761fdce584066e50c1202babbc31762326cd96d073edd7d83aa8b8a231948a3d404adb93ea8a8ea2
|
7
|
+
data.tar.gz: eaafb72d93e036a0e8af226877fea17d5489e722bbd0d403946bc4828e3429f2a37cf85dc46818116fe751fb937dc3c1851e4cc9ac55f0d7fbbb0482611abe38
|
data/README.md
CHANGED
@@ -119,6 +119,7 @@ record.review # "Lunch was great
|
|
119
119
|
* [Record setters](#record-setters)
|
120
120
|
* [Record getters](#record-getters)
|
121
121
|
* [Include linked resources (hyperlinks and hypermedia)](#include-linked-resources-hyperlinks-and-hypermedia)
|
122
|
+
* [Generate links from parameters](#generate-links-from-parameters)
|
122
123
|
* [Ensure the whole linked collection is included: includes_all](#ensure-the-whole-linked-collection-is-included-includes_all)
|
123
124
|
* [Include the first linked page or single item is included: include](#include-the-first-linked-page-or-single-item-is-included-include)
|
124
125
|
* [Include various levels of linked data](#include-various-levels-of-linked-data)
|
@@ -136,8 +137,11 @@ record.review # "Lunch was great
|
|
136
137
|
* [Disable request cycle cache](#disable-request-cycle-cache)
|
137
138
|
* [Option Blocks](#option-blocks)
|
138
139
|
* [Request tracing](#request-tracing)
|
140
|
+
* [Extended Rollbar Logging](#extended-rollbar-logging)
|
139
141
|
* [Testing with LHS](#testing-with-lhs)
|
140
|
-
* [Test helper
|
142
|
+
* [Test helper](#test-helper)
|
143
|
+
* [Stub](#stub)
|
144
|
+
* [Stub All](#stub-all)
|
141
145
|
* [Test query chains](#test-query-chains)
|
142
146
|
* [By explicitly resolving the chain: fetch](#by-explicitly-resolving-the-chain-fetch)
|
143
147
|
* [Without resolving the chain: where_values_hash](#without-resolving-the-chain-where_values_hash)
|
@@ -1627,6 +1631,15 @@ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbuck
|
|
1627
1631
|
|
1628
1632
|
-> See [record validation](#record-validation) for how to handle validation errors when updating records.
|
1629
1633
|
|
1634
|
+
You can also pass explicit request options to `update`, by passing two explicit hashes:
|
1635
|
+
|
1636
|
+
```ruby
|
1637
|
+
# app/controllers/some_controller.rb
|
1638
|
+
|
1639
|
+
record.update({ recommended: true }, { method: 'put' })
|
1640
|
+
|
1641
|
+
```
|
1642
|
+
|
1630
1643
|
##### partial_update
|
1631
1644
|
|
1632
1645
|
`partial_update` updates just the provided parameters.
|
@@ -1658,6 +1671,15 @@ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbuck
|
|
1658
1671
|
|
1659
1672
|
-> See [record validation](#record-validation) for how to handle validation errors when updating records.
|
1660
1673
|
|
1674
|
+
You can also pass explicit request options to `partial_update`, by passing two explicit hashes:
|
1675
|
+
|
1676
|
+
```ruby
|
1677
|
+
# app/controllers/some_controller.rb
|
1678
|
+
|
1679
|
+
record.partial_update({ recommended: true }, { method: 'put' })
|
1680
|
+
|
1681
|
+
```
|
1682
|
+
|
1661
1683
|
#### Endpoint url parameter injection during record creation/change
|
1662
1684
|
|
1663
1685
|
LHS injects parameters provided to `create`, `update`, `partial_update`, `save` etc. into an endpoint's URL when matching:
|
@@ -2020,6 +2042,21 @@ With `includes` or `includes_all` (to enforce fetching all remote objects for pa
|
|
2020
2042
|
|
2021
2043
|
Including linked resources/records is heavily influenced by [https://guides.rubyonrails.org/active_record_querying.html](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) and you should read it to understand this feature in all it's glory.
|
2022
2044
|
|
2045
|
+
#### Generate links from parameters
|
2046
|
+
|
2047
|
+
Sometimes you need to generate full hrefs/urls for records but you just have parameters that describe that record, like the ID.
|
2048
|
+
|
2049
|
+
For those usecases you can use `href_for(params)`:
|
2050
|
+
|
2051
|
+
```ruby
|
2052
|
+
# app/controllers/some_controller.rb
|
2053
|
+
|
2054
|
+
Presence.create(place: { href: Place.href_for(123) })
|
2055
|
+
```
|
2056
|
+
```
|
2057
|
+
POST '/presences' { place: { href: "http://datastore/places/123" } }
|
2058
|
+
```
|
2059
|
+
|
2023
2060
|
#### Ensure the whole linked collection is included: includes_all
|
2024
2061
|
|
2025
2062
|
In case endpoints are paginated and you are certain that you'll need all objects of a set and not only the first page/batch, use `includes_all`.
|
@@ -2428,6 +2465,26 @@ code.places
|
|
2428
2465
|
}
|
2429
2466
|
```
|
2430
2467
|
|
2468
|
+
## Extended Rollbar Logging
|
2469
|
+
|
2470
|
+
In order to log all requests/responses prior to an exception reported by Rollbar in addition to the exception itself, use the `LHS::ExtendedRollbar` interceptor in combination with the rollbar processor/handler:
|
2471
|
+
|
2472
|
+
```ruby
|
2473
|
+
# config/initializers/lhc.rb
|
2474
|
+
|
2475
|
+
LHC.configure do |config|
|
2476
|
+
config.interceptors = [LHS::ExtendedRollbar]
|
2477
|
+
end
|
2478
|
+
```
|
2479
|
+
|
2480
|
+
```ruby
|
2481
|
+
# config/initializers/rollbar.rb
|
2482
|
+
|
2483
|
+
Rollbar.configure do |config|
|
2484
|
+
config.before_process << LHS::Interceptors::ExtendedRollbar::Handler.init
|
2485
|
+
end
|
2486
|
+
```
|
2487
|
+
|
2431
2488
|
## Testing with LHS
|
2432
2489
|
|
2433
2490
|
**Best practice in regards of testing applications using LHS, is to let LHS fetch your records, actually perform HTTP requests and [WebMock](https://github.com/bblimke/webmock) to stub/mock those http requests/responses.**
|
@@ -2465,17 +2522,44 @@ it 'displays contracts' do
|
|
2465
2522
|
end
|
2466
2523
|
```
|
2467
2524
|
|
2468
|
-
### Test helper
|
2525
|
+
### Test helper
|
2469
2526
|
|
2470
|
-
In order to
|
2527
|
+
In order to load LHS test helpers into your tests, add the following to your spec helper:
|
2471
2528
|
|
2472
2529
|
```ruby
|
2473
2530
|
# spec/spec_helper.rb
|
2474
2531
|
|
2475
|
-
require 'lhs/
|
2532
|
+
require 'lhs/rspec'
|
2476
2533
|
```
|
2477
2534
|
|
2478
|
-
This will
|
2535
|
+
This e.g. will prevent running into caching issues during your tests, when (request cycle cache)[#request-cycle-cache] is enabled.
|
2536
|
+
It will initialize a MemoryStore cache for LHC::Caching interceptor and resets the cache before every test.
|
2537
|
+
|
2538
|
+
#### Stub
|
2539
|
+
|
2540
|
+
LHS offers stub helpers that simplify stubbing https request to your apis.
|
2541
|
+
|
2542
|
+
##### Stub All
|
2543
|
+
|
2544
|
+
`LHS.stub.all(url, items, additional_options)`
|
2545
|
+
|
2546
|
+
```ruby
|
2547
|
+
# your_spec.rb
|
2548
|
+
|
2549
|
+
before do
|
2550
|
+
LHS.stub.all(
|
2551
|
+
'https://records',
|
2552
|
+
200.times.map{ |index| { name: "Item #{index}" } },
|
2553
|
+
headers: {
|
2554
|
+
'Authorization' => 'Bearer 123'
|
2555
|
+
}
|
2556
|
+
)
|
2557
|
+
end
|
2558
|
+
```
|
2559
|
+
```
|
2560
|
+
GET https://records?limit=100
|
2561
|
+
GET https://records?limit=100&offset=100
|
2562
|
+
```
|
2479
2563
|
|
2480
2564
|
### Test query chains
|
2481
2565
|
|
data/lhs.gemspec
CHANGED
@@ -28,9 +28,11 @@ Gem::Specification.new do |s|
|
|
28
28
|
|
29
29
|
s.add_development_dependency 'capybara'
|
30
30
|
s.add_development_dependency 'json', '>= 1.8.2'
|
31
|
+
s.add_development_dependency 'local_uri'
|
31
32
|
s.add_development_dependency 'pry'
|
32
33
|
s.add_development_dependency 'pry-byebug'
|
33
34
|
s.add_development_dependency 'rails', '>= 4.2.11'
|
35
|
+
s.add_development_dependency 'rollbar'
|
34
36
|
s.add_development_dependency 'rspec-rails', '>= 3.7.0'
|
35
37
|
s.add_development_dependency 'rubocop', '~> 0.57.1'
|
36
38
|
s.add_development_dependency 'rubocop-rspec', '~> 1.26.0'
|
data/lib/lhs.rb
CHANGED
@@ -15,10 +15,28 @@ module LHS
|
|
15
15
|
'lhs/config'
|
16
16
|
autoload :Data,
|
17
17
|
'lhs/data'
|
18
|
+
autoload :ExtendedRollbar,
|
19
|
+
'lhs/interceptors/extended_rollbar/interceptor'
|
18
20
|
autoload :Endpoint,
|
19
21
|
'lhs/endpoint'
|
20
22
|
autoload :Inspect,
|
21
23
|
'lhs/concerns/inspect'
|
24
|
+
module Interceptors
|
25
|
+
module RequestCycleCache
|
26
|
+
autoload :ThreadRegistry,
|
27
|
+
'lhs/interceptors/request_cycle_cache/thread_registry'
|
28
|
+
autoload :Interceptor,
|
29
|
+
'lhs/interceptors/request_cycle_cache/interceptor'
|
30
|
+
end
|
31
|
+
module ExtendedRollbar
|
32
|
+
autoload :ThreadRegistry,
|
33
|
+
'lhs/interceptors/extended_rollbar/thread_registry'
|
34
|
+
autoload :Interceptor,
|
35
|
+
'lhs/interceptors/extended_rollbar/interceptor'
|
36
|
+
autoload :Handler,
|
37
|
+
'lhs/interceptors/extended_rollbar/handler'
|
38
|
+
end
|
39
|
+
end
|
22
40
|
autoload :IsHref,
|
23
41
|
'lhs/concerns/is_href'
|
24
42
|
autoload :Item,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_support'
|
4
|
+
require 'active_support/per_thread_registry'
|
4
5
|
|
5
6
|
module LHS
|
6
7
|
module OptionBlocks
|
@@ -8,7 +9,7 @@ module LHS
|
|
8
9
|
|
9
10
|
class CurrentOptionBlock
|
10
11
|
# Using ActiveSupports PerThreadRegistry to be able to support Active Support v4.
|
11
|
-
# Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support
|
12
|
+
# Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support v4.
|
12
13
|
extend ActiveSupport::PerThreadRegistry
|
13
14
|
attr_accessor :options
|
14
15
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
class LHS::Record
|
6
|
+
|
7
|
+
module HrefFor
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def href_for(args = nil)
|
12
|
+
return unless [Integer, String].include?(args.class)
|
13
|
+
params = { id: args }
|
14
|
+
find_endpoint(params).compile(params)
|
15
|
+
end
|
16
|
+
alias url_for href_for
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -509,16 +509,16 @@ class LHS::Record
|
|
509
509
|
options
|
510
510
|
end
|
511
511
|
|
512
|
-
# Injects options into request, that enable the
|
512
|
+
# Injects options into request, that enable the request cycle cache interceptor
|
513
513
|
def inject_request_cycle_cache!(options)
|
514
514
|
return unless LHS.config.request_cycle_cache_enabled
|
515
515
|
interceptors = options[:interceptors] || LHC.config.interceptors
|
516
516
|
if interceptors.include?(LHC::Caching)
|
517
|
-
# Ensure
|
518
|
-
interceptors = interceptors.unshift(LHS::
|
517
|
+
# Ensure interceptor is prepend
|
518
|
+
interceptors = interceptors.unshift(LHS::Interceptors::RequestCycleCache::Interceptor)
|
519
519
|
options[:interceptors] = interceptors
|
520
520
|
else
|
521
|
-
warn("[WARNING] Can't enable
|
521
|
+
warn("[WARNING] Can't enable request cycle cache as LHC::Caching interceptor is not enabled/configured (see https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md#caching-interceptor)!")
|
522
522
|
end
|
523
523
|
end
|
524
524
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LHS
|
4
|
+
module Interceptors
|
5
|
+
module ExtendedRollbar
|
6
|
+
class Handler
|
7
|
+
|
8
|
+
def self.init
|
9
|
+
proc do |options|
|
10
|
+
# as handlers cant influence what actually is reported to rollbar
|
11
|
+
# this just makes sure that Rollbar is already loaded when this class is loaded,
|
12
|
+
# so that we can extend rollbar loging
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ExtendedLogging
|
17
|
+
def log(level, *args)
|
18
|
+
args[2] = {} if args[2].nil?
|
19
|
+
args[2][:lhs] = LHS::Interceptors::ExtendedRollbar::ThreadRegistry.log.map do |entry|
|
20
|
+
{
|
21
|
+
request: entry[:request].options,
|
22
|
+
response: {
|
23
|
+
code: entry[:response].code,
|
24
|
+
body: entry[:response].body
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end.to_json
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ::Rollbar
|
33
|
+
class Notifier
|
34
|
+
prepend LHS::Interceptors::ExtendedRollbar::Handler::ExtendedLogging
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module LHS
|
6
|
+
module Interceptors
|
7
|
+
module ExtendedRollbar
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class Interceptor < LHC::Interceptor
|
11
|
+
def after_response
|
12
|
+
return unless LHS::Interceptors::ExtendedRollbar::ThreadRegistry.log
|
13
|
+
LHS::Interceptors::ExtendedRollbar::ThreadRegistry.log.push(request: request, response: response)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
const_set('ExtendedRollbar', LHS::Interceptors::ExtendedRollbar::Interceptor)
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/per_thread_registry'
|
5
|
+
|
6
|
+
module LHS
|
7
|
+
module Interceptors
|
8
|
+
module ExtendedRollbar
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
class ThreadRegistry
|
12
|
+
# Using ActiveSupports PerThreadRegistry to be able to support Active Support v4.
|
13
|
+
# Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support v4.
|
14
|
+
extend ActiveSupport::PerThreadRegistry
|
15
|
+
attr_accessor :log
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module LHS
|
6
|
+
module Interceptors
|
7
|
+
module RequestCycleCache
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class Interceptor < LHC::Interceptor
|
11
|
+
|
12
|
+
VERSION = 1
|
13
|
+
CACHED_METHODS = [:get].freeze
|
14
|
+
|
15
|
+
def before_request
|
16
|
+
request.options = {
|
17
|
+
cache: {
|
18
|
+
expires_in: 5.minutes,
|
19
|
+
race_condition_ttl: 5.seconds,
|
20
|
+
key: cache_key_for(request),
|
21
|
+
methods: CACHED_METHODS,
|
22
|
+
use: LHS.config.request_cycle_cache
|
23
|
+
}
|
24
|
+
}.merge(request.options)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def cache_key_for(request)
|
30
|
+
[
|
31
|
+
"LHS_REQUEST_CYCLE_CACHE(v#{VERSION})",
|
32
|
+
request.method.upcase,
|
33
|
+
[request.url, request.params.presence].compact.join('?'),
|
34
|
+
"REQUEST=#{LHS::Interceptors::RequestCycleCache::ThreadRegistry.request_id}",
|
35
|
+
"HEADERS=#{request.headers.hash}"
|
36
|
+
].join(' ')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/per_thread_registry'
|
5
|
+
|
6
|
+
module LHS
|
7
|
+
module Interceptors
|
8
|
+
module RequestCycleCache
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
class ThreadRegistry
|
11
|
+
# Using ActiveSupports PerThreadRegistry to be able to support Active Support v4.
|
12
|
+
# Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support v4.
|
13
|
+
extend ActiveSupport::PerThreadRegistry
|
14
|
+
attr_accessor :request_id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/lhs/railtie.rb
CHANGED
@@ -8,6 +8,7 @@ module LHS
|
|
8
8
|
def initialize
|
9
9
|
prepare_lhs_request_cycle_cache
|
10
10
|
reset_option_blocks
|
11
|
+
reset_extended_rollbar_request_logs
|
11
12
|
super
|
12
13
|
end
|
13
14
|
|
@@ -15,12 +16,18 @@ module LHS
|
|
15
16
|
|
16
17
|
def prepare_lhs_request_cycle_cache
|
17
18
|
return unless LHS.config.request_cycle_cache_enabled
|
18
|
-
LHS::
|
19
|
+
LHS::Interceptors::RequestCycleCache::ThreadRegistry.request_id = [Time.now.to_f, request.object_id].join('#')
|
19
20
|
end
|
20
21
|
|
21
22
|
def reset_option_blocks
|
22
23
|
LHS::OptionBlocks::CurrentOptionBlock.options = nil
|
23
24
|
end
|
25
|
+
|
26
|
+
def reset_extended_rollbar_request_logs
|
27
|
+
return unless defined?(::Rollbar)
|
28
|
+
return unless LHC.config.interceptors.include?(LHS::Interceptors::ExtendedRollbar::Interceptor)
|
29
|
+
LHS::Interceptors::ExtendedRollbar::ThreadRegistry.log = []
|
30
|
+
end
|
24
31
|
end
|
25
32
|
end
|
26
33
|
end
|
data/lib/lhs/record.rb
CHANGED
@@ -23,6 +23,8 @@ class LHS::Record
|
|
23
23
|
'lhs/concerns/record/find_by'
|
24
24
|
autoload :First,
|
25
25
|
'lhs/concerns/record/first'
|
26
|
+
autoload :HrefFor,
|
27
|
+
'lhs/concerns/record/href_for'
|
26
28
|
autoload :Last,
|
27
29
|
'lhs/concerns/record/last'
|
28
30
|
autoload :Mapping,
|
@@ -46,13 +48,6 @@ class LHS::Record
|
|
46
48
|
autoload :AttributeAssignment,
|
47
49
|
'lhs/concerns/record/attribute_assignment'
|
48
50
|
|
49
|
-
module RequestCycleCache
|
50
|
-
autoload :RequestCycleThreadRegistry,
|
51
|
-
'lhs/concerns/record/request_cycle_cache/request_cycle_thread_registry'
|
52
|
-
autoload :Interceptor,
|
53
|
-
'lhs/concerns/record/request_cycle_cache/interceptor'
|
54
|
-
end
|
55
|
-
|
56
51
|
include Batch
|
57
52
|
include Chainable
|
58
53
|
include Configuration
|
@@ -64,6 +59,7 @@ class LHS::Record
|
|
64
59
|
include Find
|
65
60
|
include FindBy
|
66
61
|
include First
|
62
|
+
include HrefFor
|
67
63
|
include LHS::IsHref
|
68
64
|
include Last
|
69
65
|
include LHS::Inspect
|
@@ -74,7 +70,6 @@ class LHS::Record
|
|
74
70
|
include Provider
|
75
71
|
include Request
|
76
72
|
include Relations
|
77
|
-
include RequestCycleCache
|
78
73
|
include Scope
|
79
74
|
include Tracing
|
80
75
|
include AttributeAssignment
|
data/lib/lhs/rspec.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lhs'
|
4
|
+
require 'lhs/test/stub'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.before(:each) do
|
8
|
+
LHS.config.request_cycle_cache.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
config.before(:all) do
|
12
|
+
|
13
|
+
module LHS
|
14
|
+
def self.stub
|
15
|
+
LHS::Test::Stub
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'local_uri'
|
4
|
+
require 'webmock'
|
5
|
+
module LHS
|
6
|
+
module Test
|
7
|
+
class Stub
|
8
|
+
extend WebMock::API
|
9
|
+
DEFAULT_LIMIT = LHS::Pagination::Base::DEFAULT_LIMIT
|
10
|
+
|
11
|
+
def self.all(url, items, options = {})
|
12
|
+
items.each_slice(DEFAULT_LIMIT).with_index do |(*batch), index|
|
13
|
+
uri = LocalUri::URI.new(url)
|
14
|
+
uri.query.merge!(limit: DEFAULT_LIMIT)
|
15
|
+
uri.query.merge!(offset: DEFAULT_LIMIT * index) unless index.zero?
|
16
|
+
stub_request(:get, uri.to_s)
|
17
|
+
.with(options)
|
18
|
+
.to_return(
|
19
|
+
body: {
|
20
|
+
items: batch,
|
21
|
+
offset: index * DEFAULT_LIMIT,
|
22
|
+
total: items.length
|
23
|
+
}.to_json
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/lhs/version.rb
CHANGED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ExtendedRollbarController < ApplicationController
|
4
|
+
|
5
|
+
def extended_rollbar
|
6
|
+
Record.where(color: 'blue').fetch
|
7
|
+
Record.where(color: 'red').fetch
|
8
|
+
raise "Let's see if rollbar logs information about what kind of requests where made around here!"
|
9
|
+
end
|
10
|
+
end
|
data/spec/dummy/config/routes.rb
CHANGED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
describe 'Extended Rollbar', type: :request do
|
6
|
+
let!(:records_request_1) do
|
7
|
+
stub_request(:get, "http://datastore/v2/records?color=blue").to_return(body: ['blue'].to_json)
|
8
|
+
end
|
9
|
+
|
10
|
+
let!(:records_request_2) do
|
11
|
+
stub_request(:get, "http://datastore/v2/records?color=red").to_return(body: ['red'].to_json)
|
12
|
+
end
|
13
|
+
|
14
|
+
let!(:rollbar_request) do
|
15
|
+
stub_request(:post, "https://api.rollbar.com/api/1/item/")
|
16
|
+
.with do |request|
|
17
|
+
json = JSON.parse request.body
|
18
|
+
message = "Let's see if rollbar logs information about what kind of requests where made around here!"
|
19
|
+
extra = {
|
20
|
+
lhs: [
|
21
|
+
{
|
22
|
+
request: {
|
23
|
+
params: { color: 'blue' },
|
24
|
+
url: 'http://datastore/v2/records',
|
25
|
+
headers: {
|
26
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
27
|
+
'Accept' => 'application/json,application/vnd.api+json',
|
28
|
+
'Accept-Charset' => 'utf-8'
|
29
|
+
}
|
30
|
+
},
|
31
|
+
response: { code: 200, body: '["blue"]' }
|
32
|
+
}, {
|
33
|
+
request: {
|
34
|
+
params: { color: 'red' },
|
35
|
+
url: 'http://datastore/v2/records',
|
36
|
+
headers: {
|
37
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
38
|
+
'Accept' => 'application/json,application/vnd.api+json',
|
39
|
+
'Accept-Charset' => 'utf-8'
|
40
|
+
}
|
41
|
+
},
|
42
|
+
response: { code: 200, body: '["red"]' }
|
43
|
+
}
|
44
|
+
].to_json
|
45
|
+
}
|
46
|
+
json['access_token'] == '12345' &&
|
47
|
+
json['data']['level'] == 'error' &&
|
48
|
+
json['data']['body']['trace']['exception']['message'] == message &&
|
49
|
+
json['data']['body']['trace']['extra'].to_json == extra.to_json
|
50
|
+
end
|
51
|
+
.to_return(status: 200)
|
52
|
+
end
|
53
|
+
|
54
|
+
before do
|
55
|
+
LHC.configure do |config|
|
56
|
+
config.interceptors = [LHS::ExtendedRollbar]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'extends default rollbar logging by adding information about the requests made during a request/response cycle',
|
61
|
+
dummy_models: true, extended_rollbar: true do
|
62
|
+
get '/extended_rollbar'
|
63
|
+
expect(records_request_1).to have_been_requested
|
64
|
+
expect(records_request_2).to have_been_requested
|
65
|
+
expect(rollbar_request).to have_been_requested
|
66
|
+
end
|
67
|
+
end
|
data/spec/rails_helper.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
describe LHS::Record do
|
6
|
+
before do
|
7
|
+
class Record < LHS::Record
|
8
|
+
endpoint 'http://datastore/records/'
|
9
|
+
endpoint 'http://datastore/records/{id}'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'href_for' do
|
14
|
+
|
15
|
+
it 'injects variables and returns href' do
|
16
|
+
expect(Record.href_for(1)).to eq 'http://datastore/records/1'
|
17
|
+
expect(Record.href_for('vmasd241')).to eq 'http://datastore/records/vmasd241'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'also works with url_for (alias)' do
|
21
|
+
expect(Record.url_for(1)).to eq 'http://datastore/records/1'
|
22
|
+
expect(Record.url_for('vmasd241')).to eq 'http://datastore/records/vmasd241'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -37,7 +37,7 @@ describe 'Request Cycle Cache', type: :request do
|
|
37
37
|
expect(lambda do
|
38
38
|
get '/request_cycle_cache/no_caching_interceptor'
|
39
39
|
end).to output(
|
40
|
-
%r{\[WARNING\] Can't enable
|
40
|
+
%r{\[WARNING\] Can't enable request cycle cache as LHC::Caching interceptor is not enabled/configured \(see https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md#caching-interceptor\)!}
|
41
41
|
).to_stderr
|
42
42
|
expect(request).to have_been_made.times(2)
|
43
43
|
end
|
@@ -52,11 +52,11 @@ describe 'Request Cycle Cache', type: :request do
|
|
52
52
|
it 'sets different uniq request ids as base for request cycle caching for different requests',
|
53
53
|
dummy_models: true, request_cycle_cache: true do
|
54
54
|
get '/request_cycle_cache/simple'
|
55
|
-
first_request_id = LHS::
|
55
|
+
first_request_id = LHS::Interceptors::RequestCycleCache::ThreadRegistry.request_id
|
56
56
|
second_request_id = nil
|
57
57
|
thread = Thread.new do
|
58
58
|
get '/request_cycle_cache/simple'
|
59
|
-
second_request_id = LHS::
|
59
|
+
second_request_id = LHS::Interceptors::RequestCycleCache::ThreadRegistry.request_id
|
60
60
|
end
|
61
61
|
thread.join
|
62
62
|
expect(first_request_id).not_to be_nil
|
@@ -70,7 +70,7 @@ describe 'Request Cycle Cache', type: :request do
|
|
70
70
|
expect(lambda do
|
71
71
|
get '/request_cycle_cache/no_caching_interceptor'
|
72
72
|
end).not_to output(
|
73
|
-
%r{\[WARNING\] Can't enable
|
73
|
+
%r{\[WARNING\] Can't enable request cycle cache as LHC::Caching interceptor is not enabled/configured \(see https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md#caching-interceptor\)!}
|
74
74
|
).to_stderr
|
75
75
|
expect(request).to have_been_made.times(2)
|
76
76
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
require 'lhs/rspec'
|
5
|
+
|
6
|
+
describe LHS do
|
7
|
+
|
8
|
+
before do
|
9
|
+
class Record < LHS::Record
|
10
|
+
endpoint 'https://records'
|
11
|
+
end
|
12
|
+
|
13
|
+
LHS.stub.all(
|
14
|
+
'https://records',
|
15
|
+
200.times.map { |index| { name: "Item #{index}" } },
|
16
|
+
headers: {
|
17
|
+
'Authorization' => 'Bearer 123'
|
18
|
+
}
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'stubs all requests' do
|
23
|
+
records = Record.options(headers: { 'Authorization' => 'Bearer 123' }).all.fetch
|
24
|
+
expect(records.count).to eq 200
|
25
|
+
expect(records.length).to eq 200
|
26
|
+
expect(records.first.name).to eq 'Item 0'
|
27
|
+
end
|
28
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lhs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 20.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- https://github.com/local-ch/lhs/graphs/contributors
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 1.8.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: local_uri
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: pry
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,20 @@ dependencies:
|
|
122
136
|
- - ">="
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: 4.2.11
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rollbar
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
125
153
|
- !ruby/object:Gem::Dependency
|
126
154
|
name: rspec-rails
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -252,6 +280,7 @@ files:
|
|
252
280
|
- lib/lhs/concerns/record/find.rb
|
253
281
|
- lib/lhs/concerns/record/find_by.rb
|
254
282
|
- lib/lhs/concerns/record/first.rb
|
283
|
+
- lib/lhs/concerns/record/href_for.rb
|
255
284
|
- lib/lhs/concerns/record/last.rb
|
256
285
|
- lib/lhs/concerns/record/mapping.rb
|
257
286
|
- lib/lhs/concerns/record/merge.rb
|
@@ -260,13 +289,16 @@ files:
|
|
260
289
|
- lib/lhs/concerns/record/provider.rb
|
261
290
|
- lib/lhs/concerns/record/relations.rb
|
262
291
|
- lib/lhs/concerns/record/request.rb
|
263
|
-
- lib/lhs/concerns/record/request_cycle_cache/interceptor.rb
|
264
|
-
- lib/lhs/concerns/record/request_cycle_cache/request_cycle_thread_registry.rb
|
265
292
|
- lib/lhs/concerns/record/scope.rb
|
266
293
|
- lib/lhs/concerns/record/tracing.rb
|
267
294
|
- lib/lhs/config.rb
|
268
295
|
- lib/lhs/data.rb
|
269
296
|
- lib/lhs/endpoint.rb
|
297
|
+
- lib/lhs/interceptors/extended_rollbar/handler.rb
|
298
|
+
- lib/lhs/interceptors/extended_rollbar/interceptor.rb
|
299
|
+
- lib/lhs/interceptors/extended_rollbar/thread_registry.rb
|
300
|
+
- lib/lhs/interceptors/request_cycle_cache/interceptor.rb
|
301
|
+
- lib/lhs/interceptors/request_cycle_cache/thread_registry.rb
|
270
302
|
- lib/lhs/item.rb
|
271
303
|
- lib/lhs/pagination/base.rb
|
272
304
|
- lib/lhs/pagination/link.rb
|
@@ -282,7 +314,8 @@ files:
|
|
282
314
|
- lib/lhs/proxy.rb
|
283
315
|
- lib/lhs/railtie.rb
|
284
316
|
- lib/lhs/record.rb
|
285
|
-
- lib/lhs/
|
317
|
+
- lib/lhs/rspec.rb
|
318
|
+
- lib/lhs/test/stub.rb
|
286
319
|
- lib/lhs/unprocessable.rb
|
287
320
|
- lib/lhs/version.rb
|
288
321
|
- script/ci/build.sh
|
@@ -321,6 +354,7 @@ files:
|
|
321
354
|
- spec/dummy/app/controllers/application_controller.rb
|
322
355
|
- spec/dummy/app/controllers/concerns/.keep
|
323
356
|
- spec/dummy/app/controllers/error_handling_with_chains_controller.rb
|
357
|
+
- spec/dummy/app/controllers/extended_rollbar_controller.rb
|
324
358
|
- spec/dummy/app/controllers/option_blocks_controller.rb
|
325
359
|
- spec/dummy/app/controllers/request_cycle_cache_controller.rb
|
326
360
|
- spec/dummy/app/helpers/application_helper.rb
|
@@ -349,6 +383,7 @@ files:
|
|
349
383
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
350
384
|
- spec/dummy/config/initializers/inflections.rb
|
351
385
|
- spec/dummy/config/initializers/mime_types.rb
|
386
|
+
- spec/dummy/config/initializers/rollbar.rb
|
352
387
|
- spec/dummy/config/initializers/session_store.rb
|
353
388
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
354
389
|
- spec/dummy/config/locales/en.yml
|
@@ -361,6 +396,7 @@ files:
|
|
361
396
|
- spec/dummy/public/500.html
|
362
397
|
- spec/dummy/public/favicon.ico
|
363
398
|
- spec/endpoint/for_url_spec.rb
|
399
|
+
- spec/extended_rollbar_spec.rb
|
364
400
|
- spec/item/access_errors_spec.rb
|
365
401
|
- spec/item/accessors_spec.rb
|
366
402
|
- spec/item/add_error_spec.rb
|
@@ -427,6 +463,7 @@ files:
|
|
427
463
|
- spec/record/handle_includes_errors_spec.rb
|
428
464
|
- spec/record/has_many_spec.rb
|
429
465
|
- spec/record/has_one_spec.rb
|
466
|
+
- spec/record/href_for_spec.rb
|
430
467
|
- spec/record/ignore_errors_spec.rb
|
431
468
|
- spec/record/immutable_chains_spec.rb
|
432
469
|
- spec/record/includes_all_spec.rb
|
@@ -465,6 +502,7 @@ files:
|
|
465
502
|
- spec/request_cycle_cache_spec.rb
|
466
503
|
- spec/require_lhs_spec.rb
|
467
504
|
- spec/spec_helper.rb
|
505
|
+
- spec/stubs/all_spec.rb
|
468
506
|
- spec/support/fixtures/json/feedback.json
|
469
507
|
- spec/support/fixtures/json/feedbacks.json
|
470
508
|
- spec/support/fixtures/json/localina_content_ad.json
|
@@ -492,8 +530,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
492
530
|
version: '0'
|
493
531
|
requirements:
|
494
532
|
- Ruby >= 2.3.0
|
495
|
-
|
496
|
-
rubygems_version: 2.7.8
|
533
|
+
rubygems_version: 3.0.3
|
497
534
|
signing_key:
|
498
535
|
specification_version: 4
|
499
536
|
summary: 'REST services accelerator: Rails gem providing an easy, active-record-like
|
@@ -533,6 +570,7 @@ test_files:
|
|
533
570
|
- spec/dummy/app/controllers/application_controller.rb
|
534
571
|
- spec/dummy/app/controllers/concerns/.keep
|
535
572
|
- spec/dummy/app/controllers/error_handling_with_chains_controller.rb
|
573
|
+
- spec/dummy/app/controllers/extended_rollbar_controller.rb
|
536
574
|
- spec/dummy/app/controllers/option_blocks_controller.rb
|
537
575
|
- spec/dummy/app/controllers/request_cycle_cache_controller.rb
|
538
576
|
- spec/dummy/app/helpers/application_helper.rb
|
@@ -561,6 +599,7 @@ test_files:
|
|
561
599
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
562
600
|
- spec/dummy/config/initializers/inflections.rb
|
563
601
|
- spec/dummy/config/initializers/mime_types.rb
|
602
|
+
- spec/dummy/config/initializers/rollbar.rb
|
564
603
|
- spec/dummy/config/initializers/session_store.rb
|
565
604
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
566
605
|
- spec/dummy/config/locales/en.yml
|
@@ -573,6 +612,7 @@ test_files:
|
|
573
612
|
- spec/dummy/public/500.html
|
574
613
|
- spec/dummy/public/favicon.ico
|
575
614
|
- spec/endpoint/for_url_spec.rb
|
615
|
+
- spec/extended_rollbar_spec.rb
|
576
616
|
- spec/item/access_errors_spec.rb
|
577
617
|
- spec/item/accessors_spec.rb
|
578
618
|
- spec/item/add_error_spec.rb
|
@@ -639,6 +679,7 @@ test_files:
|
|
639
679
|
- spec/record/handle_includes_errors_spec.rb
|
640
680
|
- spec/record/has_many_spec.rb
|
641
681
|
- spec/record/has_one_spec.rb
|
682
|
+
- spec/record/href_for_spec.rb
|
642
683
|
- spec/record/ignore_errors_spec.rb
|
643
684
|
- spec/record/immutable_chains_spec.rb
|
644
685
|
- spec/record/includes_all_spec.rb
|
@@ -677,6 +718,7 @@ test_files:
|
|
677
718
|
- spec/request_cycle_cache_spec.rb
|
678
719
|
- spec/require_lhs_spec.rb
|
679
720
|
- spec/spec_helper.rb
|
721
|
+
- spec/stubs/all_spec.rb
|
680
722
|
- spec/support/fixtures/json/feedback.json
|
681
723
|
- spec/support/fixtures/json/feedbacks.json
|
682
724
|
- spec/support/fixtures/json/localina_content_ad.json
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_support'
|
4
|
-
|
5
|
-
class LHS::Record
|
6
|
-
|
7
|
-
module RequestCycleCache
|
8
|
-
extend ActiveSupport::Concern
|
9
|
-
|
10
|
-
class Interceptor < LHC::Interceptor
|
11
|
-
|
12
|
-
VERSION = 1
|
13
|
-
CACHED_METHODS = [:get].freeze
|
14
|
-
|
15
|
-
def before_request
|
16
|
-
request.options = request.options.merge({
|
17
|
-
cache: {
|
18
|
-
expires_in: 5.minutes,
|
19
|
-
race_condition_ttl: 5.seconds,
|
20
|
-
key: cache_key_for(request),
|
21
|
-
methods: CACHED_METHODS,
|
22
|
-
use: LHS.config.request_cycle_cache
|
23
|
-
}
|
24
|
-
}.merge(request.options))
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def cache_key_for(request)
|
30
|
-
[
|
31
|
-
"LHS_REQUEST_CYCLE_CACHE(v#{VERSION})",
|
32
|
-
request.method.upcase,
|
33
|
-
[request.url, request.params.presence].compact.join('?'),
|
34
|
-
"REQUEST=#{LHS::Record::RequestCycleCache::RequestCycleThreadRegistry.request_id}",
|
35
|
-
"HEADERS=#{request.headers.hash}"
|
36
|
-
].join(' ')
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_support'
|
4
|
-
|
5
|
-
class LHS::Record
|
6
|
-
|
7
|
-
module RequestCycleCache
|
8
|
-
class RequestCycleThreadRegistry
|
9
|
-
# Using ActiveSupports PerThreadRegistry to be able to support Active Support v4.
|
10
|
-
# Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support v5.
|
11
|
-
extend ActiveSupport::PerThreadRegistry
|
12
|
-
attr_accessor :request_id
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|