lutaml-hal 0.1.5 → 0.1.6
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/Gemfile +4 -3
- data/README.adoc +171 -5
- data/lib/lutaml/hal/model_register.rb +78 -7
- data/lib/lutaml/hal/page.rb +138 -0
- data/lib/lutaml/hal/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 173922c9afbab790441aa0b40fc7a2401c78c72cbcc55d8cf3a7de737e12ea88
|
4
|
+
data.tar.gz: 8fdde395a4ac1898f2df9a259210127a3955e8d49d6b2cf3b710649daae0d3d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9bfd6e3a62e7359b4e0ad38ed72b3f300d23640d44f31942a5a178ef0bec5617d1d6202360bc9ec6cee10ff911e62a81752659ed5820c25e731e8304dbd7158
|
7
|
+
data.tar.gz: 6b256a2a898e2ed123c267ac7ee93780e0aa77b3715832e9f15856c0d9f600b1d05d080d469569060c086d348fc9a294d5d9eff6c6269c838d0ecd9883988eb5
|
data/Gemfile
CHANGED
data/README.adoc
CHANGED
@@ -552,16 +552,28 @@ You can define endpoints for collections (index) and individual resources
|
|
552
552
|
|
553
553
|
The `add_endpoint` method takes the following parameters:
|
554
554
|
|
555
|
+
|
555
556
|
`id`:: A unique identifier for the endpoint.
|
557
|
+
|
556
558
|
`type`:: The type of endpoint, which can be `index` or `resource`.
|
557
|
-
`url`:: The URL of the endpoint, which can include path parameters.
|
558
|
-
`model`:: The class of the resource that will be fetched from the API.
|
559
|
-
The class must inherit from `Lutaml::Hal::Resource`.
|
560
559
|
|
560
|
+
`url`:: The URL of the endpoint, which can include path parameters.
|
561
|
+
+
|
561
562
|
In the `url`, you can use interpolation parameters, which will be replaced with
|
562
563
|
the actual values when fetching the resource. The interpolation parameters are
|
563
564
|
defined in the `url` string using curly braces `{}`.
|
564
565
|
|
566
|
+
`model`:: The class of the resource that will be fetched from the API.
|
567
|
+
The class must inherit from `Lutaml::Hal::Resource`.
|
568
|
+
|
569
|
+
`query_params`:: (optional) A hash defining query parameters that should be
|
570
|
+
appended to the URL when fetching the resource. Supports parameter templates
|
571
|
+
using curly braces `{}` for dynamic values.
|
572
|
+
+
|
573
|
+
This is essential for APIs that require query parameters for pagination,
|
574
|
+
filtering, or other functionality where the same base URL needs different query
|
575
|
+
parameters to access different resources or views.
|
576
|
+
|
565
577
|
The `add_endpoint` method will automatically handle the URL resolution and fetch
|
566
578
|
the resource from the API.
|
567
579
|
|
@@ -592,6 +604,35 @@ register.add_endpoint( <1>
|
|
592
604
|
<5> The `model` is the class of the resource that will be fetched from
|
593
605
|
the API. The class must inherit from `Lutaml::Hal::Resource`.
|
594
606
|
|
607
|
+
|
608
|
+
.Example of registering and using query parameters
|
609
|
+
[example]
|
610
|
+
====
|
611
|
+
[source,ruby]
|
612
|
+
----
|
613
|
+
# Register an endpoint that supports pagination via query parameters
|
614
|
+
register.add_endpoint(
|
615
|
+
id: :product_index,
|
616
|
+
type: :index,
|
617
|
+
url: '/products',
|
618
|
+
model: ProductIndex,
|
619
|
+
query_params: {
|
620
|
+
'page' => '{page}',
|
621
|
+
'items' => '{items}'
|
622
|
+
}
|
623
|
+
)
|
624
|
+
|
625
|
+
# Fetch the first page with 10 items per page
|
626
|
+
page_1 = register.fetch(:product_index, page: 1, items: 10)
|
627
|
+
# => client.get('/products?page=1&items=10')
|
628
|
+
|
629
|
+
# Fetch the second page with 5 items per page
|
630
|
+
page_2 = register.fetch(:product_index, page: 2, items: 5)
|
631
|
+
# => client.get('/products?page=2&items=5')
|
632
|
+
----
|
633
|
+
====
|
634
|
+
|
635
|
+
|
595
636
|
.Example of registering the Product class to both index and resource endpoints
|
596
637
|
[example]
|
597
638
|
====
|
@@ -614,6 +655,34 @@ register.add_endpoint(
|
|
614
655
|
====
|
615
656
|
|
616
657
|
|
658
|
+
.Example of using query_params for pagination
|
659
|
+
[example]
|
660
|
+
====
|
661
|
+
[source,ruby]
|
662
|
+
----
|
663
|
+
# Register an endpoint that supports pagination via query parameters
|
664
|
+
register.add_endpoint(
|
665
|
+
id: :product_index_paginated,
|
666
|
+
type: :index,
|
667
|
+
url: '/products',
|
668
|
+
model: ProductIndex,
|
669
|
+
query_params: {
|
670
|
+
'page' => '{page}',
|
671
|
+
'items' => '{items}'
|
672
|
+
}
|
673
|
+
)
|
674
|
+
|
675
|
+
# Fetch the first page with 10 items per page
|
676
|
+
page_1 = register.fetch(:product_index_paginated, page: 1, items: 10)
|
677
|
+
# => client.get('/products?page=1&items=10')
|
678
|
+
|
679
|
+
# Fetch the second page with 5 items per page
|
680
|
+
page_2 = register.fetch(:product_index_paginated, page: 2, items: 5)
|
681
|
+
# => client.get('/products?page=2&items=5')
|
682
|
+
----
|
683
|
+
====
|
684
|
+
|
685
|
+
|
617
686
|
[[defining_hal_page_models]]
|
618
687
|
=== Defining HAL page models
|
619
688
|
|
@@ -920,12 +989,87 @@ product_2_related_1 = product_2.links.related.first.realize
|
|
920
989
|
|
921
990
|
=== Handling HAL pages / pagination
|
922
991
|
|
992
|
+
==== General
|
993
|
+
|
923
994
|
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
|
924
995
|
|
925
996
|
As described in <<defining_hal_page_models>>, subclassing the `Page` class
|
926
997
|
provides pagination capabilities, including the management of links to navigate
|
927
998
|
through pages of resources.
|
928
999
|
|
1000
|
+
==== Pagination navigation methods
|
1001
|
+
|
1002
|
+
The `Page` class provides several convenience methods for navigating through
|
1003
|
+
paginated results:
|
1004
|
+
|
1005
|
+
`#next_page`:: Returns the next page link if available, `nil` otherwise.
|
1006
|
+
|
1007
|
+
`#prev_page`:: Returns the previous page link if available, `nil` otherwise.
|
1008
|
+
|
1009
|
+
`#first_page`:: Returns the first page link if available, `nil` otherwise.
|
1010
|
+
|
1011
|
+
`#last_page`:: Returns the last page link if available, `nil` otherwise.
|
1012
|
+
|
1013
|
+
These methods return `Link` objects that can be realized using the `realize` method:
|
1014
|
+
|
1015
|
+
[source,ruby]
|
1016
|
+
----
|
1017
|
+
# Navigate to next page
|
1018
|
+
if current_page.next_page
|
1019
|
+
next_page = current_page.next_page.realize
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
# Navigate to previous page
|
1023
|
+
if current_page.prev_page
|
1024
|
+
prev_page = current_page.prev_page.realize
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# Jump to first or last page
|
1028
|
+
first_page = current_page.first_page.realize if current_page.first_page
|
1029
|
+
last_page = current_page.last_page.realize if current_page.last_page
|
1030
|
+
----
|
1031
|
+
|
1032
|
+
==== Pagination helper methods
|
1033
|
+
|
1034
|
+
The `Page` class also provides helper methods to check the availability of
|
1035
|
+
navigation links:
|
1036
|
+
|
1037
|
+
`#has_next?`:: Returns `true` if there is a next page available, `false`
|
1038
|
+
otherwise.
|
1039
|
+
|
1040
|
+
`#has_prev?`:: Returns `true` if there is a previous page available, `false`
|
1041
|
+
otherwise.
|
1042
|
+
|
1043
|
+
`#has_first?`:: Returns `true` if there is a first page link available, `false`
|
1044
|
+
otherwise.
|
1045
|
+
|
1046
|
+
`#has_last?`:: Returns `true` if there is a last page link available, `false`
|
1047
|
+
otherwise.
|
1048
|
+
|
1049
|
+
`#total_pages`:: Returns the total number of pages (alias for the `pages`
|
1050
|
+
attribute).
|
1051
|
+
|
1052
|
+
|
1053
|
+
==== Exhaustive pagination
|
1054
|
+
|
1055
|
+
For scenarios where you need to process all pages of results, you can combine
|
1056
|
+
the pagination methods:
|
1057
|
+
|
1058
|
+
[source,ruby]
|
1059
|
+
----
|
1060
|
+
current_page = register.fetch(:resource_index)
|
1061
|
+
|
1062
|
+
while current_page
|
1063
|
+
# Process current page
|
1064
|
+
puts "Processing page #{current_page.page} of #{current_page.total_pages}"
|
1065
|
+
|
1066
|
+
# Move to next page
|
1067
|
+
current_page = current_page.next
|
1068
|
+
end
|
1069
|
+
----
|
1070
|
+
|
1071
|
+
|
1072
|
+
==== Usage
|
929
1073
|
|
930
1074
|
.Usage example of the Page class
|
931
1075
|
[example]
|
@@ -980,11 +1124,33 @@ page_1
|
|
980
1124
|
# next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
981
1125
|
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
|
982
1126
|
|
1127
|
+
# Check if navigation is available
|
1128
|
+
page_1.has_next? # => true
|
1129
|
+
page_1.has_prev? # => false
|
1130
|
+
page_1.total_pages # => 10
|
1131
|
+
|
1132
|
+
# Navigate using convenience methods
|
1133
|
+
page_2 = page_1.next
|
1134
|
+
# => client.get('/resources?page=2&items=10')
|
1135
|
+
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100, ...>
|
1136
|
+
|
1137
|
+
page_2.has_prev? # => true
|
1138
|
+
page_2.has_next? # => true
|
1139
|
+
|
1140
|
+
# Navigate back to first page
|
1141
|
+
first_page = page_2.first
|
1142
|
+
# => client.get('/resources?page=1&items=10')
|
1143
|
+
|
1144
|
+
# Jump to last page
|
1145
|
+
last_page = page_2.last
|
1146
|
+
# => client.get('/resources?page=10&items=10')
|
1147
|
+
|
1148
|
+
# Alternative: using link realization (original method)
|
983
1149
|
# Without a GlobalRegister
|
984
|
-
page_2 =
|
1150
|
+
page_2 = page_1.links.next.realize(register)
|
985
1151
|
|
986
1152
|
# With a GlobalRegister
|
987
|
-
page_2 =
|
1153
|
+
page_2 = page_1.links.next.realize
|
988
1154
|
|
989
1155
|
# => client.get('/resources?page=2&items=10')
|
990
1156
|
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
|
@@ -16,11 +16,11 @@ module Lutaml
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# Register a model with its base URL pattern
|
19
|
-
def add_endpoint(id:, type:, url:, model:)
|
19
|
+
def add_endpoint(id:, type:, url:, model:, query_params: nil)
|
20
20
|
@models ||= {}
|
21
21
|
|
22
22
|
raise "Model with ID #{id} already registered" if @models[id]
|
23
|
-
if @models.values.any? { |m| m[:url] == url && m[:type] == type }
|
23
|
+
if @models.values.any? { |m| m[:url] == url && m[:type] == type && m[:query_params] == query_params }
|
24
24
|
raise "Duplicate URL pattern #{url} for type #{type}"
|
25
25
|
end
|
26
26
|
|
@@ -28,7 +28,8 @@ module Lutaml
|
|
28
28
|
id: id,
|
29
29
|
type: type,
|
30
30
|
url: url,
|
31
|
-
model: model
|
31
|
+
model: model,
|
32
|
+
query_params: query_params
|
32
33
|
}
|
33
34
|
end
|
34
35
|
|
@@ -38,7 +39,7 @@ module Lutaml
|
|
38
39
|
raise 'Client not configured' unless client
|
39
40
|
|
40
41
|
url = interpolate_url(endpoint[:url], params)
|
41
|
-
response = client.get(url)
|
42
|
+
response = client.get(build_url_with_query_params(url, endpoint[:query_params], params))
|
42
43
|
|
43
44
|
realized_model = endpoint[:model].from_json(response.to_json)
|
44
45
|
|
@@ -103,12 +104,82 @@ module Lutaml
|
|
103
104
|
end
|
104
105
|
end
|
105
106
|
|
107
|
+
def build_url_with_query_params(base_url, query_params_template, params)
|
108
|
+
return base_url unless query_params_template
|
109
|
+
|
110
|
+
query_params = []
|
111
|
+
query_params_template.each do |param_name, param_template|
|
112
|
+
# If the template is like {page}, look for the param in the passed params
|
113
|
+
if param_template.is_a?(String) && param_template.match?(/\{(.+)\}/)
|
114
|
+
param_key = param_template.match(/\{(.+)\}/)[1]
|
115
|
+
query_params << "#{param_name}=#{params[param_key.to_sym]}" if params[param_key.to_sym]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
query_params.any? ? "#{base_url}?#{query_params.join('&')}" : base_url
|
120
|
+
end
|
121
|
+
|
106
122
|
def find_matching_model_class(href)
|
107
123
|
@models.values.find do |model_data|
|
108
|
-
|
124
|
+
matches_url_with_params?(model_data, href)
|
109
125
|
end&.[](:model)
|
110
126
|
end
|
111
127
|
|
128
|
+
def matches_url_with_params?(model_data, href)
|
129
|
+
pattern = model_data[:url]
|
130
|
+
query_params = model_data[:query_params]
|
131
|
+
|
132
|
+
return false unless pattern && href
|
133
|
+
|
134
|
+
uri = parse_href_uri(href)
|
135
|
+
pattern_path = extract_pattern_path(pattern)
|
136
|
+
|
137
|
+
return false unless path_matches?(pattern_path, uri.path)
|
138
|
+
return true unless query_params
|
139
|
+
|
140
|
+
query_params_match?(query_params, parse_query_params(uri.query))
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_href_uri(href)
|
144
|
+
full_href = href.start_with?('http') ? href : "#{client&.api_url}#{href}"
|
145
|
+
URI.parse(full_href)
|
146
|
+
end
|
147
|
+
|
148
|
+
def extract_pattern_path(pattern)
|
149
|
+
pattern.split('?').first
|
150
|
+
end
|
151
|
+
|
152
|
+
def path_matches?(pattern_path, href_path)
|
153
|
+
if href_path.start_with?('/') && client&.api_url
|
154
|
+
path_pattern = extract_path(pattern_path)
|
155
|
+
pattern_match?(path_pattern, href_path) || pattern_match?(pattern_path, href_path)
|
156
|
+
else
|
157
|
+
pattern_match?(pattern_path, href_path)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def query_params_match?(expected_params, actual_params)
|
162
|
+
expected_params.all? do |param_name, param_pattern|
|
163
|
+
actual_value = actual_params[param_name]
|
164
|
+
next false unless actual_value
|
165
|
+
|
166
|
+
template_param?(param_pattern) || actual_value == param_pattern.to_s
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def template_param?(param_pattern)
|
171
|
+
param_pattern.is_a?(String) && param_pattern.match?(/\{.+\}/)
|
172
|
+
end
|
173
|
+
|
174
|
+
def parse_query_params(query_string)
|
175
|
+
return {} unless query_string
|
176
|
+
|
177
|
+
query_string.split('&').each_with_object({}) do |param, hash|
|
178
|
+
key, value = param.split('=', 2)
|
179
|
+
hash[key] = value if key
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
112
183
|
def matches_url?(pattern, href)
|
113
184
|
return false unless pattern && href
|
114
185
|
|
@@ -134,8 +205,8 @@ module Lutaml
|
|
134
205
|
|
135
206
|
# Convert {param} to wildcards for matching
|
136
207
|
pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
|
137
|
-
# Convert * wildcards to regex pattern
|
138
|
-
regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '
|
208
|
+
# Convert * wildcards to regex pattern - use .+ instead of [^/]+ to match query parameters
|
209
|
+
regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '.+')}$")
|
139
210
|
|
140
211
|
Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
|
141
212
|
Hal.debug_log("pattern_match?: href to match #{url}")
|
data/lib/lutaml/hal/page.rb
CHANGED
@@ -32,6 +32,144 @@ module Lutaml
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
|
+
|
36
|
+
# Returns the next page of results, or nil if on the last page
|
37
|
+
#
|
38
|
+
# @return [Object, nil] The next page instance or nil
|
39
|
+
def next
|
40
|
+
return nil unless links.next
|
41
|
+
|
42
|
+
links.next.realize
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the previous page of results, or nil if on the first page
|
46
|
+
#
|
47
|
+
# @return [Object, nil] The previous page instance or nil
|
48
|
+
def prev
|
49
|
+
# If the API provides a prev link, use it
|
50
|
+
return links.prev.realize if links.prev
|
51
|
+
|
52
|
+
# If we're on page 1, there's no previous page
|
53
|
+
return nil if page <= 1
|
54
|
+
|
55
|
+
# Construct the previous page URL manually
|
56
|
+
prev_page_url = construct_page_url(page - 1)
|
57
|
+
return nil unless prev_page_url
|
58
|
+
|
59
|
+
# Use the HAL register to fetch the previous page
|
60
|
+
register_name = instance_variable_get("@#{Lutaml::Hal::REGISTER_ID_ATTR_NAME}")
|
61
|
+
return nil unless register_name
|
62
|
+
|
63
|
+
hal_register = Lutaml::Hal::GlobalRegister.instance.get(register_name)
|
64
|
+
return nil unless hal_register
|
65
|
+
|
66
|
+
hal_register.resolve_and_cast(nil, prev_page_url)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the first page of results
|
70
|
+
#
|
71
|
+
# @return [Object, nil] The first page instance or nil
|
72
|
+
def first
|
73
|
+
return nil unless links.first
|
74
|
+
|
75
|
+
links.first.realize
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the last page of results
|
79
|
+
#
|
80
|
+
# @return [Object, nil] The last page instance or nil
|
81
|
+
def last
|
82
|
+
return nil unless links.last
|
83
|
+
|
84
|
+
links.last.realize
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the total number of pages
|
88
|
+
#
|
89
|
+
# @return [Integer] The total number of pages
|
90
|
+
def total_pages
|
91
|
+
pages
|
92
|
+
end
|
93
|
+
|
94
|
+
# Checks if there is a next page available
|
95
|
+
#
|
96
|
+
# @return [Boolean] true if next page exists, false otherwise
|
97
|
+
def next?
|
98
|
+
!links.next.nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Checks if there is a previous page available
|
102
|
+
#
|
103
|
+
# @return [Boolean] true if previous page exists, false otherwise
|
104
|
+
def prev?
|
105
|
+
!links.prev.nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Checks if there is a first page link available
|
109
|
+
#
|
110
|
+
# @return [Boolean] true if first page link exists, false otherwise
|
111
|
+
def first?
|
112
|
+
!links.first.nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Checks if there is a last page link available
|
116
|
+
#
|
117
|
+
# @return [Boolean] true if last page link exists, false otherwise
|
118
|
+
def last?
|
119
|
+
!links.last.nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the next page link, or nil if on the last page
|
123
|
+
#
|
124
|
+
# @return [Object, nil] The next page link or nil
|
125
|
+
def next_page
|
126
|
+
links.next
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the previous page link, or nil if on the first page
|
130
|
+
#
|
131
|
+
# @return [Object, nil] The previous page link or nil
|
132
|
+
def prev_page
|
133
|
+
links.prev
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns the first page link
|
137
|
+
#
|
138
|
+
# @return [Object, nil] The first page link or nil
|
139
|
+
def first_page
|
140
|
+
links.first
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the last page link
|
144
|
+
#
|
145
|
+
# @return [Object, nil] The last page link or nil
|
146
|
+
def last_page
|
147
|
+
links.last
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Constructs a URL for a specific page based on the current page's URL pattern
|
153
|
+
#
|
154
|
+
# @param target_page [Integer] The page number to construct URL for
|
155
|
+
# @return [String, nil] The constructed URL or nil if unable to construct
|
156
|
+
def construct_page_url(target_page)
|
157
|
+
# Try to get a reference URL from next, first, or last links
|
158
|
+
reference_url = links.next&.href || links.first&.href || links.last&.href
|
159
|
+
return nil unless reference_url
|
160
|
+
|
161
|
+
# Parse the reference URL and modify the page parameter
|
162
|
+
uri = URI.parse(reference_url)
|
163
|
+
query_params = URI.decode_www_form(uri.query || '')
|
164
|
+
|
165
|
+
# Update the page parameter
|
166
|
+
query_params = query_params.reject { |key, _| key == 'page' }
|
167
|
+
query_params << ['page', target_page.to_s]
|
168
|
+
|
169
|
+
# Reconstruct the URL
|
170
|
+
uri.query = URI.encode_www_form(query_params)
|
171
|
+
uri.to_s
|
172
|
+
end
|
35
173
|
end
|
36
174
|
end
|
37
175
|
end
|
data/lib/lutaml/hal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml-hal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03
|
11
|
+
date: 2025-07-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|