webfunction 1.0.0
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 +7 -0
- data/.rubocop.yml +42 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +598 -0
- data/Rakefile +12 -0
- data/lib/web_function.rb +1 -0
- data/lib/webfunction/argument.rb +123 -0
- data/lib/webfunction/attribute.rb +113 -0
- data/lib/webfunction/client.rb +155 -0
- data/lib/webfunction/documented_error.rb +67 -0
- data/lib/webfunction/endpoint.rb +243 -0
- data/lib/webfunction/flaggable.rb +40 -0
- data/lib/webfunction/object_schema.rb +135 -0
- data/lib/webfunction/package.rb +196 -0
- data/lib/webfunction/page.rb +134 -0
- data/lib/webfunction/pipeline.rb +91 -0
- data/lib/webfunction/promise.rb +150 -0
- data/lib/webfunction/request.rb +177 -0
- data/lib/webfunction/type/any.rb +48 -0
- data/lib/webfunction/type/array_of.rb +58 -0
- data/lib/webfunction/type/base.rb +89 -0
- data/lib/webfunction/type/union.rb +54 -0
- data/lib/webfunction/type.rb +140 -0
- data/lib/webfunction/utils.rb +67 -0
- data/lib/webfunction/version.rb +5 -0
- data/lib/webfunction.rb +52 -0
- data/sig/webfunction.rbs +4 -0
- metadata +131 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebFunction
|
|
4
|
+
# A module that provides a flaggable interface. Flags are used to define the behavior of an object.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class Endpoint
|
|
8
|
+
# include Flaggable
|
|
9
|
+
#
|
|
10
|
+
# def initialize(name:, flags: [])
|
|
11
|
+
# @name = name
|
|
12
|
+
# @flags = flags
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# endpoint = Endpoint.new(name: "get_user", flags: ["private"])
|
|
17
|
+
# endpoint.flag?("private") # => true
|
|
18
|
+
# endpoint.flag?("public") # => false
|
|
19
|
+
#
|
|
20
|
+
module Flaggable
|
|
21
|
+
# List of flags. See the [available flags section][2] on the Web Function
|
|
22
|
+
# website for a complete list of flags available.
|
|
23
|
+
#
|
|
24
|
+
# @return [Array<String>]
|
|
25
|
+
#
|
|
26
|
+
# [2]: https://webfunction.org/package#available-flags
|
|
27
|
+
#
|
|
28
|
+
attr_reader :flags
|
|
29
|
+
|
|
30
|
+
# Whether the endpoint declares the given flag.
|
|
31
|
+
#
|
|
32
|
+
# @param flag [String] The flag to check for.
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
#
|
|
36
|
+
def flag?(flag)
|
|
37
|
+
@flags.include?(flag)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebFunction
|
|
4
|
+
# Represents a named object definition as described in a Web Function package.
|
|
5
|
+
#
|
|
6
|
+
# Objects are declared in a package under the `"objects"` key and can be referenced as a refined object type
|
|
7
|
+
# (`object.<name>`) anywhere a type is expected. An `object.` reference appears in one of two contexts, which
|
|
8
|
+
# determines which set of properties applies:
|
|
9
|
+
#
|
|
10
|
+
# - Argument context — the object is referenced as an argument's `type`. Its {#arguments} describe its properties.
|
|
11
|
+
# - Attribute context — the object is referenced as an endpoint's `returns` or as an attribute's `type`. Its
|
|
12
|
+
# {#attributes} describe its properties.
|
|
13
|
+
#
|
|
14
|
+
# Because an object MAY be referenced in both contexts within the same package, it MAY define both `arguments` and
|
|
15
|
+
# `attributes`; each set is used only in its matching context.
|
|
16
|
+
#
|
|
17
|
+
# It is named `ObjectSchema` rather than `Object` to avoid clashing with Ruby's built-in `::Object`.
|
|
18
|
+
#
|
|
19
|
+
# See the [object definition documentation][0] on the Web Function website for more details.
|
|
20
|
+
#
|
|
21
|
+
# [0]: https://webfunction.org/package#object-definition
|
|
22
|
+
#
|
|
23
|
+
class ObjectSchema
|
|
24
|
+
# The contexts in which an object may be referenced. Each maps to the member set that applies in that context.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Symbol>]
|
|
27
|
+
#
|
|
28
|
+
CONTEXTS = %i[arguments attributes].freeze
|
|
29
|
+
|
|
30
|
+
def initialize(name:, arguments: [], attributes: [])
|
|
31
|
+
@name = name
|
|
32
|
+
@arguments = arguments.to_h { |a| [a.name, a] }
|
|
33
|
+
@attributes = attributes.to_h { |a| [a.name, a] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Creates a new ObjectSchema from a hash. Typically coming from a {Package}.
|
|
38
|
+
#
|
|
39
|
+
# @param object [Hash] The object hash
|
|
40
|
+
#
|
|
41
|
+
# @return [ObjectSchema, nil] A new ObjectSchema instance, or `nil` if the hash is invalid.
|
|
42
|
+
#
|
|
43
|
+
def from_hash(object)
|
|
44
|
+
unless object.is_a?(Hash)
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless object["name"]
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
new(
|
|
53
|
+
name: object["name"],
|
|
54
|
+
arguments: Argument.from_array(object["arguments"]),
|
|
55
|
+
attributes: Attribute.from_array(object["attributes"]),
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates a new ObjectSchema from an array of hashes. Typically coming from a {Package}. Uses
|
|
60
|
+
# {ObjectSchema#from_hash} under the hood.
|
|
61
|
+
#
|
|
62
|
+
# @param objects [Array<Hash>] The object array of hashes
|
|
63
|
+
#
|
|
64
|
+
# @return [Array<ObjectSchema>] A new array of ObjectSchema instances
|
|
65
|
+
#
|
|
66
|
+
def from_array(objects)
|
|
67
|
+
Utils.normalize_array objects do |object|
|
|
68
|
+
from_hash(object)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The name of the object. It is referenced as a refined object type (`object.<name>`) and is unique within a
|
|
74
|
+
# package.
|
|
75
|
+
#
|
|
76
|
+
# @return [String]
|
|
77
|
+
#
|
|
78
|
+
attr_reader :name
|
|
79
|
+
|
|
80
|
+
# The object's properties when it is referenced in an argument context.
|
|
81
|
+
#
|
|
82
|
+
# @return [Array<Argument>]
|
|
83
|
+
#
|
|
84
|
+
def arguments
|
|
85
|
+
@arguments.values
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Looks up a single argument member by name.
|
|
89
|
+
#
|
|
90
|
+
# @param name [String, Symbol] The name of the argument to look up.
|
|
91
|
+
#
|
|
92
|
+
# @return [Argument, nil] The matching argument, or `nil` if none is found.
|
|
93
|
+
#
|
|
94
|
+
def argument(name)
|
|
95
|
+
@arguments[name.to_s]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The object's properties when it is referenced in an attribute context.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Attribute>]
|
|
101
|
+
#
|
|
102
|
+
def attributes
|
|
103
|
+
@attributes.values
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Looks up a single attribute member by name.
|
|
107
|
+
#
|
|
108
|
+
# @param name [String, Symbol] The name of the attribute to look up.
|
|
109
|
+
#
|
|
110
|
+
# @return [Attribute, nil] The matching attribute, or `nil` if none is found.
|
|
111
|
+
#
|
|
112
|
+
def attribute(name)
|
|
113
|
+
@attributes[name.to_s]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The object's properties for the given context.
|
|
117
|
+
#
|
|
118
|
+
# @param context [Symbol] The context to resolve properties for. One of {CONTEXTS}.
|
|
119
|
+
#
|
|
120
|
+
# @raise [ArgumentError] If the context is not one of {CONTEXTS}.
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<Argument>, Array<Attribute>] The properties that apply in the given context.
|
|
123
|
+
#
|
|
124
|
+
def properties(context)
|
|
125
|
+
case context
|
|
126
|
+
when :arguments
|
|
127
|
+
arguments
|
|
128
|
+
when :attributes
|
|
129
|
+
attributes
|
|
130
|
+
else
|
|
131
|
+
raise ArgumentError, "context must be one of #{CONTEXTS.inspect}, got #{context.inspect}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebFunction
|
|
4
|
+
# Organize, document, and validate endpoints. A package facilitates {Endpoint} discovery and integration by providing
|
|
5
|
+
# standardized metadata about them.
|
|
6
|
+
#
|
|
7
|
+
# A package bundles a base URL together with the endpoints it exposes, as well as optional metadata such as a name,
|
|
8
|
+
# version information, top-level documentation, and a list of common errors.
|
|
9
|
+
#
|
|
10
|
+
# See the [package specification][0] on the Web Function website for the full description of every recognized key and
|
|
11
|
+
# its constraints.
|
|
12
|
+
#
|
|
13
|
+
# [0]: https://webfunction.org/package
|
|
14
|
+
#
|
|
15
|
+
class Package
|
|
16
|
+
include Flaggable
|
|
17
|
+
|
|
18
|
+
def initialize(base_url:, pipeline_url: nil, name: nil, version: nil, docs: nil, flags: [], versions: [],
|
|
19
|
+
endpoints: [], errors: [], objects: [])
|
|
20
|
+
@base_url = base_url
|
|
21
|
+
@pipeline_url = pipeline_url
|
|
22
|
+
@name = name
|
|
23
|
+
@version = version
|
|
24
|
+
@docs = docs.to_s
|
|
25
|
+
@flags = flags
|
|
26
|
+
@versions = versions
|
|
27
|
+
@endpoints = endpoints.to_h { |e| [e.name, e] }
|
|
28
|
+
@errors = errors.to_h { |e| [e.code, e] }
|
|
29
|
+
@objects = objects.to_h { |o| [o.name, o] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Instantiate a new Package from a hash.
|
|
34
|
+
#
|
|
35
|
+
# @param package [Hash] The package hash
|
|
36
|
+
#
|
|
37
|
+
# @return [Package] A new Package instance
|
|
38
|
+
#
|
|
39
|
+
def from_hash(package)
|
|
40
|
+
new(
|
|
41
|
+
base_url: package["base_url"],
|
|
42
|
+
pipeline_url: package["pipeline_url"],
|
|
43
|
+
name: package["name"],
|
|
44
|
+
version: package["version"],
|
|
45
|
+
docs: package["docs"],
|
|
46
|
+
flags: Utils.normalize_array_of_strings(package["flags"]),
|
|
47
|
+
versions: Utils.normalize_array_of_strings(package["versions"]),
|
|
48
|
+
endpoints: Endpoint.from_array(package["endpoints"]),
|
|
49
|
+
errors: DocumentedError.from_array(package["errors"]),
|
|
50
|
+
objects: ObjectSchema.from_array(package["objects"]),
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The base URL for the package. Endpoint URLs are formed by joining this base URL with each endpoint's name.
|
|
56
|
+
#
|
|
57
|
+
# This is required for the package to be valid and MUST use the HTTP or HTTPS scheme.
|
|
58
|
+
#
|
|
59
|
+
# @return [String]
|
|
60
|
+
#
|
|
61
|
+
attr_reader :base_url
|
|
62
|
+
|
|
63
|
+
# A function pipelining URL used to batch several endpoint invocations into a single request. See the pipelining
|
|
64
|
+
# specification for more information.
|
|
65
|
+
#
|
|
66
|
+
# @return [String, nil]
|
|
67
|
+
#
|
|
68
|
+
attr_reader :pipeline_url
|
|
69
|
+
|
|
70
|
+
# The name of the package.
|
|
71
|
+
#
|
|
72
|
+
# @return [String, nil]
|
|
73
|
+
#
|
|
74
|
+
attr_reader :name
|
|
75
|
+
|
|
76
|
+
# The version that this package describes. An opaque string.
|
|
77
|
+
#
|
|
78
|
+
# This MUST be present when the `versioned` flag is set. See the versioning specification for more information.
|
|
79
|
+
#
|
|
80
|
+
# @return [String, nil]
|
|
81
|
+
#
|
|
82
|
+
attr_reader :version
|
|
83
|
+
|
|
84
|
+
# Top-level documentation for the package. It must be formatted as markdown.
|
|
85
|
+
#
|
|
86
|
+
# @return [String]
|
|
87
|
+
#
|
|
88
|
+
attr_reader :docs
|
|
89
|
+
|
|
90
|
+
# The versions that are available. Each entry is an opaque string.
|
|
91
|
+
#
|
|
92
|
+
# This MUST be present when the `versioned` flag is set. See the versioning specification for more information.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<String>]
|
|
95
|
+
#
|
|
96
|
+
attr_reader :versions
|
|
97
|
+
|
|
98
|
+
# The {Pipeline} for this package, built from {#pipeline_url}, or `nil` when the package does not declare a
|
|
99
|
+
# pipeline URL.
|
|
100
|
+
#
|
|
101
|
+
# @return [Pipeline, nil]
|
|
102
|
+
#
|
|
103
|
+
def pipeline
|
|
104
|
+
unless pipeline_url
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Pipeline.new(pipeline_url)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The endpoints declared by this package.
|
|
112
|
+
#
|
|
113
|
+
# @return [Array<Endpoint>]
|
|
114
|
+
#
|
|
115
|
+
def endpoints
|
|
116
|
+
@endpoints.values
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Looks up a single endpoint by name. Underscores in the given name are converted to hyphens so that Ruby-style
|
|
120
|
+
# names (e.g. `:find_user_by`) match the hyphenated endpoint names used in packages (e.g. `find-user-by`).
|
|
121
|
+
#
|
|
122
|
+
# @param name [String, Symbol] The name of the endpoint to look up.
|
|
123
|
+
#
|
|
124
|
+
# @return [Endpoint, nil] The matching endpoint, or `nil` if none is found.
|
|
125
|
+
#
|
|
126
|
+
def endpoint(name)
|
|
127
|
+
@endpoints[name.to_s.gsub("_", "-")]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The list of common errors that can be returned by any endpoint in this package. Only refer to this list if an
|
|
131
|
+
# endpoint uses the `error_triple` flag. See the error specification for more information.
|
|
132
|
+
#
|
|
133
|
+
# @return [Array<DocumentedError>]
|
|
134
|
+
#
|
|
135
|
+
def errors
|
|
136
|
+
@errors.values
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Looks up a single common error by its machine-readable code.
|
|
140
|
+
#
|
|
141
|
+
# @param code [String, Symbol] The error code to look up.
|
|
142
|
+
#
|
|
143
|
+
# @return [DocumentedError, nil] The matching error, or `nil` if none is found.
|
|
144
|
+
#
|
|
145
|
+
def error(code)
|
|
146
|
+
@errors[code.to_s]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The named object definitions declared by this package. Each can be referenced as a refined object type
|
|
150
|
+
# (`object.<name>`) anywhere a type is expected.
|
|
151
|
+
#
|
|
152
|
+
# @return [Array<ObjectSchema>]
|
|
153
|
+
#
|
|
154
|
+
def objects
|
|
155
|
+
@objects.values
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Looks up a single object definition by name, resolved for the given context.
|
|
159
|
+
#
|
|
160
|
+
# An `object.` reference appears in either an argument or attribute context, which determines which set of members
|
|
161
|
+
# applies. The `context` selects the member set that is relevant; an object that does not define members for the
|
|
162
|
+
# requested context is treated as absent and `nil` is returned.
|
|
163
|
+
#
|
|
164
|
+
# @param name [String, Symbol] The name of the object to look up.
|
|
165
|
+
# @param context [Symbol] The context the object is referenced in. One of {ObjectSchema::CONTEXTS}
|
|
166
|
+
# (`:arguments` or `:attributes`).
|
|
167
|
+
#
|
|
168
|
+
# @raise [ArgumentError] If the context is not one of {ObjectSchema::CONTEXTS}.
|
|
169
|
+
#
|
|
170
|
+
# @return [ObjectSchema, nil] The matching object, or `nil` if none is found or it defines no members for the
|
|
171
|
+
# given context.
|
|
172
|
+
#
|
|
173
|
+
def object(name, context:)
|
|
174
|
+
object = @objects[name.to_s]
|
|
175
|
+
|
|
176
|
+
unless object
|
|
177
|
+
return
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if object.properties(context).empty?
|
|
181
|
+
return
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
object
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Whether the package is versioned, i.e. whether it declares the `versioned` flag. A versioned package is selected
|
|
188
|
+
# using the `Api-Version` header.
|
|
189
|
+
#
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
#
|
|
192
|
+
def versioned?
|
|
193
|
+
flag?("versioned")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebFunction
|
|
4
|
+
# A page of results from a paginated endpoint.
|
|
5
|
+
#
|
|
6
|
+
# Paginated responses follow the Web Function pagination contract: a JSON object with
|
|
7
|
+
# `page`, `next`, and `previous` keys. {Request} detects this shape and returns a {Page}
|
|
8
|
+
# instead of a bare Hash. The `next` and `previous` values are opaque request bodies —
|
|
9
|
+
# call {#next_page} or {#previous_page} to fetch the adjacent page; do not construct or modify them.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# page = WebFunction::Request.execute(
|
|
13
|
+
# "https://api.example.com/list-people",
|
|
14
|
+
# args: { filters: { first_name: "Joe" } },
|
|
15
|
+
# )
|
|
16
|
+
# page.page # => [{ "person_id" => "...", ... }, ...]
|
|
17
|
+
# page.next? # => true
|
|
18
|
+
# next_page = page.next_page
|
|
19
|
+
# next_page.previous? # => true
|
|
20
|
+
#
|
|
21
|
+
# See the [pagination specification][0] for the full contract.
|
|
22
|
+
#
|
|
23
|
+
# [0]: https://webfunction.org/pagination
|
|
24
|
+
#
|
|
25
|
+
class Page
|
|
26
|
+
include Enumerable
|
|
27
|
+
|
|
28
|
+
def initialize(page:, next_body:, previous_body:, url:, bearer_auth: nil, version: nil)
|
|
29
|
+
@page = page
|
|
30
|
+
@next_body = next_body
|
|
31
|
+
@previous_body = previous_body
|
|
32
|
+
@url = url
|
|
33
|
+
@bearer_auth = bearer_auth
|
|
34
|
+
@version = version
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Whether +response+ matches the paginated response shape.
|
|
39
|
+
#
|
|
40
|
+
# @param response [Object] A parsed JSON response
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
#
|
|
44
|
+
def paginated?(response)
|
|
45
|
+
response.is_a?(Hash) &&
|
|
46
|
+
response.key?("page") &&
|
|
47
|
+
response.key?("next") &&
|
|
48
|
+
response.key?("previous") &&
|
|
49
|
+
response["page"].is_a?(Array) &&
|
|
50
|
+
(response["next"].nil? || response["next"].is_a?(Hash)) &&
|
|
51
|
+
(response["previous"].nil? || response["previous"].is_a?(Hash))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Wraps a paginated response in a {Page}, or returns +response+ unchanged.
|
|
55
|
+
#
|
|
56
|
+
# @param response [Object] A parsed JSON response
|
|
57
|
+
# @param request [Request] The request that produced the response
|
|
58
|
+
#
|
|
59
|
+
# @return [Page, Object]
|
|
60
|
+
#
|
|
61
|
+
def wrap(response, request:)
|
|
62
|
+
return response unless paginated?(response)
|
|
63
|
+
|
|
64
|
+
new(
|
|
65
|
+
page: response["page"],
|
|
66
|
+
next_body: response["next"],
|
|
67
|
+
previous_body: response["previous"],
|
|
68
|
+
url: request.url,
|
|
69
|
+
bearer_auth: request.bearer_auth,
|
|
70
|
+
version: request.version,
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The items on the current page.
|
|
76
|
+
#
|
|
77
|
+
# @return [Array]
|
|
78
|
+
#
|
|
79
|
+
attr_reader :page
|
|
80
|
+
|
|
81
|
+
# Whether a next page is available.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
#
|
|
85
|
+
def next?
|
|
86
|
+
!@next_body.nil?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Whether a previous page is available.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
#
|
|
93
|
+
def previous?
|
|
94
|
+
!@previous_body.nil?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Fetches the next page by posting the opaque `next` body to the same endpoint.
|
|
98
|
+
#
|
|
99
|
+
# @return [Page, nil] The next page, or `nil` if there is none
|
|
100
|
+
#
|
|
101
|
+
def next_page
|
|
102
|
+
fetch(@next_body)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Fetches the previous page by posting the opaque `previous` body to the same endpoint.
|
|
106
|
+
#
|
|
107
|
+
# @return [Page, nil] The previous page, or `nil` if there is none
|
|
108
|
+
#
|
|
109
|
+
def previous_page
|
|
110
|
+
fetch(@previous_body)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Iterates over the items on the current page.
|
|
114
|
+
#
|
|
115
|
+
# @yield [Object] Each item in {#page}
|
|
116
|
+
#
|
|
117
|
+
# @return [Enumerator, Page]
|
|
118
|
+
#
|
|
119
|
+
def each(&block)
|
|
120
|
+
return enum_for(:each) unless block
|
|
121
|
+
|
|
122
|
+
@page.each(&block)
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def fetch(body)
|
|
129
|
+
return nil if body.nil?
|
|
130
|
+
|
|
131
|
+
Request.execute(@url, bearer_auth: @bearer_auth, version: @version, args: body)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebFunction
|
|
4
|
+
# A pipeline is a sequence of steps that are executed in order.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# pipeline = WebFunction::Pipeline.new("https://pipe.example/exec")
|
|
8
|
+
# pipeline.add_step({ url: "https://a", headers: {}, body: {} })
|
|
9
|
+
# pipeline.add_step({ url: "https://b", headers: {}, body: {} })
|
|
10
|
+
# pipeline.execute(returns: :all) # => [{ "a" => 1 }, { "b" => 2 }]
|
|
11
|
+
#
|
|
12
|
+
class Pipeline
|
|
13
|
+
def initialize(url)
|
|
14
|
+
@url = url
|
|
15
|
+
@steps = []
|
|
16
|
+
@promises = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Adds a step to the pipeline.
|
|
20
|
+
#
|
|
21
|
+
# @param step [Hash] The step to add
|
|
22
|
+
#
|
|
23
|
+
# @return [Promise] A new Promise instance
|
|
24
|
+
#
|
|
25
|
+
def add_step(step)
|
|
26
|
+
n = @promises.count
|
|
27
|
+
promise = Promise.new(self, "$[#{n}]")
|
|
28
|
+
|
|
29
|
+
@steps << step
|
|
30
|
+
@promises << promise
|
|
31
|
+
|
|
32
|
+
promise
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Executes the pipeline.
|
|
36
|
+
#
|
|
37
|
+
# @param returns [String, Symbol] The return type or a JSONPath expression to return a specific value.
|
|
38
|
+
#
|
|
39
|
+
# @return [Object] The response returned by the pipeline.
|
|
40
|
+
#
|
|
41
|
+
def execute(returns: :all)
|
|
42
|
+
case returns
|
|
43
|
+
when :all
|
|
44
|
+
responses = Request.execute(@url, args: {
|
|
45
|
+
steps: @steps,
|
|
46
|
+
returns: "$",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
responses.each_with_index do |response, index|
|
|
51
|
+
@promises[index].value = response
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
reset!
|
|
55
|
+
|
|
56
|
+
responses
|
|
57
|
+
when :last
|
|
58
|
+
response = Request.execute(@url, args: {
|
|
59
|
+
steps: @steps,
|
|
60
|
+
returns: "$[-1:]",
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@promises.last.value = response
|
|
65
|
+
|
|
66
|
+
reset!
|
|
67
|
+
|
|
68
|
+
response
|
|
69
|
+
else
|
|
70
|
+
response = Request.execute(@url, args: {
|
|
71
|
+
steps: @steps,
|
|
72
|
+
returns: returns,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
reset!
|
|
77
|
+
|
|
78
|
+
response
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Resets the pipeline.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
#
|
|
86
|
+
def reset!
|
|
87
|
+
@steps = []
|
|
88
|
+
@promises = []
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|