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.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Arguments are used to define Web Function {Endpoint} request parameters.
5
+ #
6
+ # See the [arguments section][0] on the Web Function website for more details.
7
+ #
8
+ # [0]: https://webfunction.org/package#arguments
9
+ #
10
+ class Argument
11
+ include Flaggable
12
+
13
+ def initialize(name:, type:, group: nil, choices: [], flags: [], docs: nil)
14
+ @name = name
15
+ @type = Type.parse(type)
16
+ @group = group
17
+ @choices = choices
18
+ @flags = flags
19
+ @docs = docs.to_s
20
+ end
21
+
22
+ class << self
23
+ # Instantiate a new Argument from a hash, typically coming from a {Package}.
24
+ #
25
+ # @param argument [Hash]
26
+ #
27
+ # @return [Argument, nil]
28
+ #
29
+ def from_hash(argument)
30
+ unless argument.is_a?(Hash)
31
+ return
32
+ end
33
+
34
+ unless argument["name"]
35
+ return
36
+ end
37
+
38
+ unless argument["type"]
39
+ return
40
+ end
41
+
42
+ new(
43
+ name: argument["name"],
44
+ type: argument["type"],
45
+ group: argument["group"],
46
+ choices: [*argument["choices"]],
47
+ flags: Utils.normalize_array_of_strings(argument["flags"]),
48
+ docs: argument["docs"],
49
+ )
50
+ end
51
+
52
+ # Instantiate a collection of Argument from an array of hash, typically coming from a {Package}. Uses
53
+ # {Argument#from_hash} under the hood.
54
+ #
55
+ # @param arguments [Array<Hash>]
56
+ #
57
+ # @return [Array<Argument>]
58
+ #
59
+ def from_array(arguments)
60
+ Utils.normalize_array arguments do |argument|
61
+ from_hash(argument)
62
+ end
63
+ end
64
+ end
65
+
66
+ # The name of the argument.
67
+ #
68
+ # @return [String]
69
+ #
70
+ attr_reader :name
71
+
72
+ # The type of the argument. It must be one of:
73
+ #
74
+ # - object
75
+ # - array
76
+ # - string
77
+ # - number
78
+ # - boolean
79
+ #
80
+ # @return [String]
81
+ #
82
+ attr_reader :type
83
+
84
+ # A name used to categorize or group similar arguments together. This should be used by documentation tools to
85
+ # organize related arguments.
86
+ #
87
+ # @return [String]
88
+ #
89
+ attr_reader :group
90
+
91
+ # An array specifying the exact, case-sensitive values that are permitted for this argument. Each value in the
92
+ # choices array must conform to the data type specified in the argument's type key.
93
+ #
94
+ # Note that if the argument type is array, choices may contain strings or numbers representing the allowed values
95
+ # that can be included in the array.
96
+ #
97
+ # @return [Array]
98
+ #
99
+ attr_reader :choices
100
+
101
+ # Description of the argument. It must be formatted as markdown.
102
+ #
103
+ # @return [String]
104
+ #
105
+ attr_reader :docs
106
+
107
+ # Whether the argument is required.
108
+ #
109
+ # @return [Boolean]
110
+ #
111
+ def required?
112
+ flag?("required")
113
+ end
114
+
115
+ # Whether the argument is optional.
116
+ #
117
+ # @return [Boolean]
118
+ #
119
+ def optional?
120
+ !required?
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Attributes define output fields that may be produced and returned by a Web Function {Endpoint} when the type of the
5
+ # return is `object`.
6
+ #
7
+ # See the [attributes section][0] on the Web Function website for more details about attribute definitions,
8
+ # recognized keys, and usage.
9
+ #
10
+ # [0]: https://webfunction.org/package#attributes
11
+ #
12
+ class Attribute
13
+ include Flaggable
14
+
15
+ def initialize(name:, type:, values: [], flags: [], docs: nil)
16
+ @name = name
17
+ @type = Type.parse(type)
18
+ @values = values
19
+ @flags = flags
20
+ @docs = docs.to_s
21
+ end
22
+
23
+ class << self
24
+ # Creates a new Attribute from a hash. Typically coming from a {Package}.
25
+ #
26
+ # @param attribute [Hash] The attribute hash
27
+ #
28
+ # @return [Attribute] A new Attribute instance
29
+ #
30
+ def from_hash(attribute)
31
+ unless attribute.is_a?(Hash)
32
+ return
33
+ end
34
+
35
+ unless attribute["name"]
36
+ return
37
+ end
38
+
39
+ unless attribute["type"]
40
+ return
41
+ end
42
+
43
+ new(
44
+ name: attribute["name"],
45
+ type: attribute["type"],
46
+ values: [*attribute["values"]],
47
+ flags: Utils.normalize_array_of_strings(attribute["flags"]),
48
+ docs: attribute["docs"],
49
+ )
50
+ end
51
+
52
+ # Creates a new Attribute from an array of hashes. Typically coming from a {Package}. Uses {Attribute#from_hash}
53
+ # under the hood.
54
+ #
55
+ # @param attributes [Array<Hash>] The attribute array of hashes
56
+ #
57
+ # @return [Array<Attribute>] A new array of Attribute instances
58
+ #
59
+ def from_array(attributes)
60
+ Utils.normalize_array attributes do |attribute|
61
+ from_hash(attribute)
62
+ end
63
+ end
64
+ end
65
+
66
+ # The name of the attribute as it will appear in the endpoint's output
67
+ # object.
68
+ #
69
+ # This is required for the argument to be valid.
70
+ #
71
+ # @return [String]
72
+ #
73
+ attr_reader :name
74
+
75
+ # The type of value returned for this attribute. Must be one of:
76
+ #
77
+ # - object
78
+ # - array
79
+ # - string
80
+ # - number
81
+ # - boolean
82
+ #
83
+ # This is required for the argument to be valid.
84
+ #
85
+ # @return [String]
86
+ #
87
+ attr_reader :type
88
+
89
+ # An array specifying the exact, case-sensitive values that may be returned for this attribute. Each value in the
90
+ # values array must conform to the data type specified in the "type" key.
91
+ #
92
+ # This is useful for attributes that can only take a select set of values (enums or constants).
93
+ #
94
+ # @return [Array]
95
+ #
96
+ attr_reader :values
97
+
98
+ # A markdown string describing this attribute and its purpose in the output object. Used by documentation tools,
99
+ # and highly recommended.
100
+ #
101
+ # @return [String]
102
+ #
103
+ attr_reader :docs
104
+
105
+ # Whether the attribute can be null.
106
+ #
107
+ # @return [Boolean]
108
+ #
109
+ def nullable?
110
+ flag?("nullable")
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # A {Client} is a wrapper around a Web Function {Package} that provides a convenient interface for invoking endpoints.
5
+ #
6
+ # @example
7
+ # client = WebFunction::Client.from_package_endpoint("https://api.webfunction.com/package")
8
+ # client.list_items(a: "b") # => { "c" => "d" }
9
+ #
10
+ class Client < BasicObject
11
+ def initialize(base_url:, endpoints: [], package: nil, bearer_auth: nil, version: nil, pipeline: nil)
12
+ @package = package
13
+ @base_url = base_url
14
+ @endpoints = endpoints.to_h { |e| [e.gsub("-", "_").to_sym, e] }
15
+ @bearer_auth = bearer_auth
16
+ @version = version
17
+ @pipeline = pipeline
18
+ end
19
+
20
+ class << self
21
+ # Creates a new {Client} from an endpoint.
22
+ #
23
+ # @param url [String] The URL of the package endpoint
24
+ # @param bearer_auth [String] The bearer authentication token
25
+ # @param version [String] The API version to use
26
+ # @param pipelined [Boolean] Whether to have the client use call pipelining
27
+ #
28
+ # @return [Client]
29
+ #
30
+ def from_package_endpoint(url, bearer_auth: nil, version: nil, pipelined: false)
31
+ response = ::WebFunction::Request.execute(url, bearer_auth: bearer_auth, version: version)
32
+ package = ::WebFunction::Package.from_hash(response)
33
+
34
+ from_package(package, bearer_auth: bearer_auth, version: version, pipelined: pipelined)
35
+ end
36
+
37
+ # Creates a new {Client} from an url.
38
+ #
39
+ # @param url [String] The URL of the package endpoint
40
+ # @param bearer_auth [String] The bearer authentication token
41
+ # @param version [String] The API version to use
42
+ # @param pipelined [Boolean] Whether to have the client use call pipelining
43
+ #
44
+ # @return [Client]
45
+ #
46
+ def from_url(url, bearer_auth: nil, version: nil, pipelined: false)
47
+ body = ::WebFunction::Utils.get_body_from_url(url, extra_query_params: { api_version: version })
48
+ package = ::WebFunction::Package.from_hash(::JSON.parse(body))
49
+
50
+ from_package(package, bearer_auth: bearer_auth, version: version, pipelined: pipelined)
51
+ end
52
+
53
+ # Creates a new {Client} from a {Package}.
54
+ #
55
+ # @param package [Package] A package
56
+ # @param bearer_auth [String] The bearer authentication token
57
+ # @param version [String] The API version to use
58
+ # @param pipelined [Boolean] Whether to have the client use call pipelining
59
+ #
60
+ # @return [Client]
61
+ #
62
+ def from_package(package, bearer_auth: nil, version: nil, pipelined: nil)
63
+ pipeline = nil
64
+
65
+ if pipelined
66
+ pipeline = package.pipeline
67
+ end
68
+
69
+ client = new(
70
+ package: package,
71
+ base_url: package.base_url,
72
+ endpoints: package.endpoints.map(&:name),
73
+ bearer_auth: bearer_auth,
74
+ version: version,
75
+ pipeline: pipeline,
76
+ )
77
+
78
+ package.endpoints.each do |endpoint|
79
+ endpoint.client = client
80
+ end
81
+
82
+ client
83
+ end
84
+ end
85
+
86
+ # Call an endpoint by name with the given arguments.
87
+ #
88
+ # @param endpoint_name [String] The name of the endpoint to call
89
+ # @param args [Hash] The arguments to send to the endpoint
90
+ #
91
+ # @return [Object, Page] The decoded response returned by the endpoint.
92
+ # Paginated responses are wrapped in a {Page}.
93
+ #
94
+ def call(endpoint_name, args = {})
95
+ url = ::URI.join(@base_url, endpoint_name).to_s
96
+ request = ::WebFunction::Request.new(url,
97
+ bearer_auth: @bearer_auth,
98
+ version: @version,
99
+ args: args,
100
+ )
101
+
102
+ if @pipeline
103
+ @pipeline.add_step(request.as_pipeline_step)
104
+ else
105
+ request.execute
106
+ end
107
+ end
108
+
109
+ # The package that this client is wrapping.
110
+ #
111
+ # @return [Package]
112
+ #
113
+ attr_reader :package
114
+
115
+ # The bearer authentication token.
116
+ #
117
+ # @param bearer_auth [String] The bearer authentication token
118
+ #
119
+ attr_writer :bearer_auth
120
+
121
+ # The API version to use.
122
+ #
123
+ # @param version [String] The API version to use
124
+ #
125
+ attr_writer :version
126
+
127
+ # The pipeline to use.
128
+ #
129
+ # @param pipeline [Pipeline] The pipeline to use
130
+ #
131
+ attr_writer :pipeline
132
+
133
+ def methods # :nodoc:
134
+ @endpoints.keys
135
+ end
136
+
137
+ def nil? # :nodoc:
138
+ false
139
+ end
140
+
141
+ def respond_to_missing?(method_name, include_private = false) # :nodoc:
142
+ @endpoints[method_name]
143
+ end
144
+
145
+ def method_missing(method_name, *args) # :nodoc:
146
+ endpoint_name = @endpoints[method_name]
147
+
148
+ unless endpoint_name
149
+ super
150
+ end
151
+
152
+ call(endpoint_name, args.first)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Represents an error definition as described in a Web Function package.
5
+ #
6
+ # An error definition documents the possible errors that an endpoint might return, including a machine-readable error
7
+ # code and a human-readable description.
8
+ #
9
+ # See the [error definition documentation][0] on the Web Function website for more details, including recognized keys
10
+ # and usage recommendations.
11
+ #
12
+ # [0]: https://webfunction.org/package#error-definition
13
+ #
14
+ class DocumentedError
15
+ def initialize(code:, docs: nil)
16
+ @code = code
17
+ @docs = docs.to_s
18
+ end
19
+
20
+ class << self
21
+ # Creates a new DocumentedError from a hash.
22
+ #
23
+ # @param error [Hash] The error hash
24
+ #
25
+ # @return [DocumentedError] A new DocumentedError instance
26
+ #
27
+ def from_hash(error)
28
+ unless error.is_a?(Hash)
29
+ return
30
+ end
31
+
32
+ unless error["code"]
33
+ return
34
+ end
35
+
36
+ new(
37
+ code: error["code"],
38
+ docs: error["docs"],
39
+ )
40
+ end
41
+
42
+ # Creates a new DocumentedError from an array of hashes. Uses {DocumentedError#from_hash} under the hood.
43
+ #
44
+ # @param errors [Array<Hash>] The error array of hashes
45
+ #
46
+ # @return [Array<DocumentedError>] A new array of DocumentedError instances
47
+ #
48
+ def from_array(errors)
49
+ Utils.normalize_array errors do |error|
50
+ from_hash(error)
51
+ end
52
+ end
53
+ end
54
+
55
+ # The machine-readable code of the error.
56
+ #
57
+ # @return [String]
58
+ #
59
+ attr_reader :code
60
+
61
+ # The documentation of the error.
62
+ #
63
+ # @return [String]
64
+ #
65
+ attr_reader :docs
66
+ end
67
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Represents an endpoint as described in a Web Function package.
5
+ #
6
+ # An endpoint defines an operation that can be performed via a Web Function API. Endpoints declare their name,
7
+ # documentation, arguments (inputs), attributes (outputs), and the possible errors that may occur when invoking them.
8
+ #
9
+ # Endpoints are described as objects in each package under the `"endpoints"` key. For more information, see:
10
+ #
11
+ # - [Web Function package docs](https://webfunction.org/package)
12
+ # - [Web Function endpoint docs](https://webfunction.org/endpoint)
13
+ #
14
+ # This class provides methods for accessing endpoint metadata (name, docs, arguments, attributes, errors) and
15
+ # supports invocation via HTTP.
16
+ #
17
+ # Typical tasks include:
18
+ #
19
+ # - Querying endpoint name or documentation
20
+ # - Enumerating the arguments or attributes definitions
21
+ # - Invoking the endpoint through HTTP using required inputs
22
+ #
23
+ # See: https://webfunction.org/endpoint for more details on endpoint structure and contract.
24
+ #
25
+ class Endpoint
26
+ include Flaggable
27
+
28
+ def initialize(name:, returns:, flags: [], group: nil, docs: nil, arguments: [], attributes: [], errors: [])
29
+ @name = name
30
+ @returns = Type.parse(returns)
31
+ @flags = flags
32
+ @group = group
33
+ @docs = docs
34
+ @arguments = arguments.to_h { |a| [a.name, a] }
35
+ @attributes = attributes.to_h { |a| [a.name, a] }
36
+ @errors = errors.to_h { |e| [e.code, e] }
37
+ end
38
+
39
+ class << self
40
+ # Invokes an endpoint through HTTP using the given URL, bearer authentication, version, and arguments.
41
+ #
42
+ # @param url [String] The URL of the endpoint to invoke
43
+ # @param bearer_auth [String] The bearer authentication token
44
+ # @param version [String] The API version to use
45
+ # @param args [Hash] The arguments to send to the endpoint
46
+ #
47
+ # @return [Object] The response returned by the endpoint
48
+ #
49
+ def invoke(url, bearer_auth: nil, version: nil, args: {})
50
+ Request.execute(url, bearer_auth: bearer_auth, version: version, args: args)
51
+ end
52
+
53
+ # Creates a new Endpoint from a hash.
54
+ #
55
+ # @param endpoint [Hash] The endpoint hash
56
+ #
57
+ # @return [Endpoint] A new Endpoint instance
58
+ #
59
+ def from_hash(endpoint)
60
+ unless endpoint.is_a?(Hash)
61
+ return
62
+ end
63
+
64
+ unless endpoint["name"]
65
+ return
66
+ end
67
+
68
+ unless endpoint["returns"]
69
+ return
70
+ end
71
+
72
+ new(
73
+ name: endpoint["name"],
74
+ returns: endpoint["returns"],
75
+ flags: Utils.normalize_array_of_strings(endpoint["flags"]),
76
+ group: endpoint["group"],
77
+ docs: endpoint["docs"].to_s,
78
+ arguments: Argument.from_array(endpoint["arguments"]),
79
+ attributes: Attribute.from_array(endpoint["attributes"]),
80
+ errors: DocumentedError.from_array(endpoint["errors"]),
81
+ )
82
+ end
83
+
84
+ # Creates a new Endpoint from an array of hashes. Uses {Endpoint#from_hash} under the hood.
85
+ #
86
+ # @param endpoints [Array<Hash>] The endpoint array of hashes
87
+ #
88
+ # @return [Array<Endpoint>] A new array of Endpoint instances
89
+ #
90
+ def from_array(endpoints)
91
+ Utils.normalize_array endpoints do |endpoint|
92
+ from_hash(endpoint)
93
+ end
94
+ end
95
+ end
96
+
97
+ # The {Client} used to invoke this endpoint. It is assigned when the endpoint is loaded from a package and is
98
+ # required by {#call}.
99
+ #
100
+ # @return [Client, nil]
101
+ #
102
+ attr_accessor :client
103
+
104
+ # The suffix for the endpoint URL, appended to the package's base URL to form the full endpoint URL. Endpoint names
105
+ # are unique within a package; overloading (two endpoints sharing the same name) is not permitted.
106
+ #
107
+ # @return [String]
108
+ #
109
+ attr_reader :name
110
+
111
+ # The JSON type(s) returned by the endpoint. A non-empty array whose entries are each one of:
112
+ #
113
+ # - object
114
+ # - array
115
+ # - string
116
+ # - number
117
+ # - boolean
118
+ # - null
119
+ #
120
+ # @return [Array<String>]
121
+ #
122
+ attr_reader :returns
123
+
124
+ # A name used to categorize or group similar endpoints together. This should be used by documentation tools to
125
+ # organize related endpoints.
126
+ #
127
+ # @return [String, nil]
128
+ #
129
+ attr_reader :group
130
+
131
+ # Documentation for the endpoint. It must be formatted as markdown.
132
+ #
133
+ # @return [String]
134
+ #
135
+ attr_reader :docs
136
+
137
+ # Invokes the endpoint through its assigned {#client}, passing the given arguments.
138
+ #
139
+ # @param args [Hash] The arguments to send to the endpoint.
140
+ #
141
+ # @raise [RuntimeError] If no client has been assigned to the endpoint.
142
+ #
143
+ # @return [Object] The decoded response returned by the endpoint.
144
+ #
145
+ def call(args = {})
146
+ unless client
147
+ raise "Client must be set to invoke an endpoint"
148
+ end
149
+
150
+ client.call(name, args)
151
+ end
152
+
153
+ # The list of errors specific to this endpoint. Clients SHOULD only refer to this list if the endpoint uses the
154
+ # `error_triple` flag. See the error specification for more information.
155
+ #
156
+ # @return [Array<DocumentedError>]
157
+ #
158
+ def errors
159
+ @errors.values
160
+ end
161
+
162
+ # Looks up a single endpoint error by its machine-readable code.
163
+ #
164
+ # @param code [String, Symbol] The error code to look up.
165
+ #
166
+ # @return [DocumentedError, nil] The matching error, or `nil` if none is found.
167
+ #
168
+ def error(code)
169
+ @errors[code.to_s]
170
+ end
171
+
172
+ # The attributes of the object returned by the endpoint. Relevant when the endpoint returns an `object`.
173
+ #
174
+ # @return [Array<Attribute>]
175
+ #
176
+ def attributes
177
+ @attributes.values
178
+ end
179
+
180
+ # Looks up a single returned attribute by name.
181
+ #
182
+ # @param name [String, Symbol] The name of the attribute to look up.
183
+ #
184
+ # @return [Attribute, nil] The matching attribute, or `nil` if none is found.
185
+ #
186
+ def attribute(name)
187
+ @attributes[name.to_s]
188
+ end
189
+
190
+ # The arguments required by the endpoint. The array is empty when the endpoint requires no arguments.
191
+ #
192
+ # @return [Array<Argument>]
193
+ #
194
+ def arguments
195
+ @arguments.values
196
+ end
197
+
198
+ # Looks up a single argument by name.
199
+ #
200
+ # @param name [String, Symbol] The name of the argument to look up.
201
+ #
202
+ # @return [Argument, nil] The matching argument, or `nil` if none is found.
203
+ #
204
+ def argument(name)
205
+ @arguments[name.to_s]
206
+ end
207
+
208
+ # Whether the endpoint requires authentication via a bearer token, i.e. whether it declares the `bearer_auth` flag.
209
+ #
210
+ # @return [Boolean]
211
+ #
212
+ def bearer_auth?
213
+ flag?("bearer_auth")
214
+ end
215
+
216
+ # Whether the endpoint returns a bearer token in its response, i.e. whether it declares the `capture_bearer` flag.
217
+ # See the authentication specification for more information.
218
+ #
219
+ # @return [Boolean]
220
+ #
221
+ def capture_bearer?
222
+ flag?("capture_bearer")
223
+ end
224
+
225
+ # Whether the endpoint supports pagination, i.e. whether it declares the `paginated` flag.
226
+ #
227
+ # @return [Boolean]
228
+ #
229
+ def paginated?
230
+ flag?("paginated")
231
+ end
232
+
233
+ # Whether the endpoint is intended for internal use and is not part of the public API, i.e. whether it declares the
234
+ # `private` flag. Documentation tooling SHOULD omit endpoints with this flag from generated or
235
+ # published documentation.
236
+ #
237
+ # @return [Boolean]
238
+ #
239
+ def private?
240
+ flag?("private")
241
+ end
242
+ end
243
+ end