web_function 0.5.0 → 0.6.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.
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
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
+ #
2
12
  class Pipeline
3
13
  def initialize(url)
4
14
  @url = url
@@ -6,6 +16,12 @@ module WebFunction
6
16
  @promises = []
7
17
  end
8
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
+ #
9
25
  def add_step(step)
10
26
  n = @promises.count
11
27
  promise = Promise.new(self, "$[#{n}]")
@@ -16,13 +32,20 @@ module WebFunction
16
32
  promise
17
33
  end
18
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
+ #
19
41
  def execute(returns: :all)
20
42
  case returns
21
43
  when :all
22
- responses = Endpoint.invoke(@url, args: {
44
+ responses = Request.execute(@url, args: {
23
45
  steps: @steps,
24
46
  returns: "$",
25
- })
47
+ },
48
+ )
26
49
 
27
50
  responses.each_with_index do |response, index|
28
51
  @promises[index].value = response
@@ -32,10 +55,11 @@ module WebFunction
32
55
 
33
56
  responses
34
57
  when :last
35
- response = Endpoint.invoke(@url, args: {
58
+ response = Request.execute(@url, args: {
36
59
  steps: @steps,
37
60
  returns: "$[-1:]",
38
- })
61
+ },
62
+ )
39
63
 
40
64
  @promises.last.value = response
41
65
 
@@ -43,10 +67,11 @@ module WebFunction
43
67
 
44
68
  response
45
69
  else
46
- response = Endpoint.invoke(@url, args: {
70
+ response = Request.execute(@url, args: {
47
71
  steps: @steps,
48
72
  returns: returns,
49
- })
73
+ },
74
+ )
50
75
 
51
76
  reset!
52
77
 
@@ -54,6 +79,10 @@ module WebFunction
54
79
  end
55
80
  end
56
81
 
82
+ # Resets the pipeline.
83
+ #
84
+ # @return [void]
85
+ #
57
86
  def reset!
58
87
  @steps = []
59
88
  @promises = []
@@ -1,4 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebFunction
4
+ # A promise is a placeholder for a value that will be resolved later.
5
+ #
6
+ # @example
7
+ # pipeline = WebFunction::Pipeline.new("https://pipe.example/exec")
8
+ # promise = pipeline.add_step({})
9
+ # promise.resolve # => { "a" => 1 }
10
+ #
2
11
  class Promise
3
12
  def initialize(pipeline, path)
4
13
  @pipeline = pipeline
@@ -6,19 +15,42 @@ module WebFunction
6
15
  @value = nil
7
16
  end
8
17
 
18
+ # A path is a JSONPath expression that can be used to resolve a value from a response.
19
+ #
20
+ # @example
21
+ # path = WebFunction::Promise::Path.new("$[0]")
22
+ # path.to_s # => "$[0]"
23
+ # path[0] # => { "a" => 1 }
24
+ #
9
25
  class Path
10
26
  def initialize(path)
11
27
  @path = path
12
28
  end
13
29
 
30
+ # Returns the string representation of the path.
31
+ #
32
+ # @return [String] The string representation of the path
33
+ #
14
34
  def to_s
15
35
  @path
16
36
  end
17
37
 
38
+ # Returns the JSON representation of the path.
39
+ #
40
+ # @param args [Array] The arguments to pass to the JSON.generate method
41
+ #
42
+ # @return [String] The JSON representation of the path
43
+ #
18
44
  def to_json(*args)
19
45
  @path.to_json(*args)
20
46
  end
21
47
 
48
+ # Returns a new path with the given key.
49
+ #
50
+ # @param key [String, Symbol, Integer] The key to add to the path
51
+ #
52
+ # @return [Path] A new Path instance
53
+ #
22
54
  def [](key)
23
55
  case key
24
56
  when String, Symbol
@@ -30,13 +62,27 @@ module WebFunction
30
62
  end
31
63
  end
32
64
 
65
+ # Mutates the path with the given path.
66
+ #
67
+ # @param path [String] The path to mutate
68
+ #
69
+ # @return [Path] A new Path instance
70
+ #
33
71
  def mutate(path)
34
72
  Path.new(path)
35
73
  end
36
74
  end
37
75
 
76
+ # The value of the promise.
77
+ #
78
+ # @return [Object] The value of the promise
79
+ #
38
80
  attr_writer :value
39
81
 
82
+ # Returns the string representation of the promise.
83
+ #
84
+ # @return [String] The string representation of the promise
85
+ #
40
86
  def to_s
41
87
  if @value
42
88
  @value.to_s
@@ -45,6 +91,12 @@ module WebFunction
45
91
  end
46
92
  end
47
93
 
94
+ # Returns the JSON representation of the promise.
95
+ #
96
+ # @param args [Array] The arguments to pass to the JSON.generate method
97
+ #
98
+ # @return [String] The JSON representation of the promise
99
+ #
48
100
  def to_json(*args)
49
101
  if @value
50
102
  @value.to_json(*args)
@@ -53,6 +105,12 @@ module WebFunction
53
105
  end
54
106
  end
55
107
 
108
+ # Returns the value of the promise at the given key.
109
+ #
110
+ # @param key [String, Symbol, Integer] The key to resolve
111
+ #
112
+ # @return [Object] The value of the promise at the given key
113
+ #
56
114
  def [](key)
57
115
  if @value
58
116
  @value[key]
@@ -61,6 +119,12 @@ module WebFunction
61
119
  end
62
120
  end
63
121
 
122
+ # Returns the value of the promise.
123
+ #
124
+ # @raise [WebFunction::UnresolvedPromiseError] If the promise is not resolved
125
+ #
126
+ # @return [Object] The value of the promise
127
+ #
64
128
  def value
65
129
  unless @value
66
130
  raise WebFunction::UnresolvedPromiseError
@@ -69,6 +133,10 @@ module WebFunction
69
133
  @value
70
134
  end
71
135
 
136
+ # Resolves the promise.
137
+ #
138
+ # @return [Object] The value of the promise
139
+ #
72
140
  def resolve
73
141
  if @value
74
142
  return @value
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # A request allows you to invoke a Web Function endpoint via an HTTP client.
5
+ #
6
+ # @example
7
+ # request = WebFunction::Request.new("https://api.example.com/endpoint")
8
+ # request.execute # => { "a" => 1 }
9
+ #
10
+ class Request
11
+ def initialize(url, bearer_auth: nil, version: nil, args: {})
12
+ @url = url
13
+ @bearer_auth = bearer_auth
14
+ @version = version
15
+ @args = args || {}
16
+ end
17
+
18
+ class << self
19
+ # The HTTP client used to execute the request.
20
+ #
21
+ # To provide a custom HTTP client instead of the default (which uses Excon),
22
+ # set this to any object responding to #call. For example, a Proc or a lambda.
23
+ #
24
+ # The contract is:
25
+ #
26
+ # client.call(url, headers, body)
27
+ #
28
+ # - url: [String] The full URL to post to (not just the hostname or path).
29
+ # - headers:[Hash<String,String>] HTTP headers, e.g. { "Content-Type" => "application/json" }
30
+ # - body: [String] The JSON body to post.
31
+ #
32
+ # The client must return a two-element Array: [status, body]:
33
+ #
34
+ # - status: [Integer] HTTP status code (e.g. 200, 400, 500)
35
+ # - body: [String] Raw response body as a string
36
+ #
37
+ # @example
38
+ # WebFunction::Endpoint.http_client = ->(url, headers, args) {
39
+ # http_response = MyHTTP.post(url, headers: headers, body: JSON.generate(args))
40
+ # [http_response.status, http_response.body]
41
+ # }
42
+ #
43
+ attr_accessor :http_client
44
+
45
+ # Executes a request.
46
+ #
47
+ # @param url [String] The URL of the request
48
+ # @param bearer_auth [String] The bearer authentication token
49
+ # @param version [String] The API version to use
50
+ # @param args [Hash] The arguments to send to the request
51
+ #
52
+ # @return [Object] The response returned by the request
53
+ def execute(url, bearer_auth: nil, version: nil, args: {})
54
+ request = new(url, bearer_auth: bearer_auth, version: version, args: args)
55
+ request.execute
56
+ end
57
+ end
58
+
59
+ self.http_client = proc do |url, headers, body|
60
+ response = Excon.post(url, headers: headers, body: body)
61
+ [response.status, response.body]
62
+ end
63
+
64
+ # The URL of the request.
65
+ #
66
+ # @return [String] The URL of the request
67
+ #
68
+ attr_reader :url
69
+
70
+ # The bearer authentication token.
71
+ #
72
+ # @return [String] The bearer authentication token
73
+ #
74
+ attr_reader :bearer_auth
75
+
76
+ # The API version to use.
77
+ #
78
+ # @return [String] The API version to use
79
+ #
80
+ attr_reader :version
81
+
82
+ # The arguments to send to the request.
83
+ #
84
+ # @return [Hash] The arguments to send to the request
85
+ #
86
+ attr_reader :args
87
+
88
+ # The headers to send to the request.
89
+ #
90
+ # @return [Hash] The headers to send to the request
91
+ #
92
+ def headers
93
+ headers = {
94
+ "Content-Type": "application/json",
95
+ "Accept": "application/json",
96
+ "User-Agent": "webfunction/#{WebFunction::VERSION}",
97
+ }
98
+
99
+ if @bearer_auth
100
+ headers["Authorization"] = "Bearer #{@bearer_auth}"
101
+ end
102
+
103
+ if @version
104
+ headers["Api-Version"] = @version
105
+ end
106
+
107
+ headers
108
+ end
109
+
110
+ # Returns the request as a pipeline step.
111
+ #
112
+ # @return [Hash] The request as a pipeline step
113
+ #
114
+ def as_pipeline_step
115
+ {
116
+ url: @url,
117
+ headers: headers,
118
+ body: @args,
119
+ }
120
+ end
121
+
122
+ # Executes the request.
123
+ #
124
+ # @raise [WebFunction::UnexpectedStatusCodeError] If the status code is not 200 or 400
125
+ # @raise [WebFunction::JsonParseError] If the response is not valid JSON
126
+ # @raise [WebFunction::BadRequestError] If the response is a bad request
127
+ #
128
+ # @return [Object] The response returned by the request
129
+ #
130
+ def execute
131
+ status, body = self.class.http_client.call(@url, headers, JSON.generate(@args))
132
+
133
+ unless [200, 400].include?(status)
134
+ raise WebFunction::UnexpectedStatusCodeError.new("Unexpected status code (#{status})",
135
+ details: {
136
+ status_code: status,
137
+ raw_body: body,
138
+ },
139
+ )
140
+ end
141
+
142
+ begin
143
+ result = JSON.parse(body)
144
+ rescue JSON::ParserError => e
145
+ raise WebFunction::JsonParseError.new(e.message,
146
+ details: {
147
+ status_code: status,
148
+ raw_body: body,
149
+ original_exception: e,
150
+ },
151
+ )
152
+ end
153
+
154
+ if status == 400
155
+ code = "WFN_BAD_REQUEST_ERROR"
156
+ message = "Bad request"
157
+ details = { body: result }
158
+
159
+ if result.is_a?(Array) && result.length == 3 && result[0].is_a?(String) && result[1].is_a?(String)
160
+ code = result[0]
161
+ message = result[1]
162
+ details = result[2]
163
+ end
164
+
165
+ raise WebFunction::BadRequestError.new(message, code: code, details: details)
166
+ end
167
+
168
+ result
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Internal utility methods.
5
+ #
6
+ # @api private
7
+ #
8
+ module Utils
9
+ module_function
10
+
11
+ # Normalizes a collection. Yields each item to the block if a block is given, otherwise returns the item.
12
+ # Any `nil` items are removed.
13
+ #
14
+ # @param collection [Array] The collection to normalize
15
+ #
16
+ # @return [Array] The normalized array
17
+ #
18
+ def normalize_array(collection)
19
+ unless collection.is_a?(Array)
20
+ return []
21
+ end
22
+
23
+ items = collection.map do |item|
24
+ if block_given?
25
+ yield item
26
+ else
27
+ item
28
+ end
29
+ end
30
+
31
+ items.compact
32
+ end
33
+
34
+ # Normalizes an array of strings. Uses #normalize_array under the hood.
35
+ #
36
+ # @param collection [Array] The collection to normalize
37
+ #
38
+ # @return [Array] The normalized array
39
+ #
40
+ def normalize_array_of_strings(collection)
41
+ normalize_array(collection) { |item| item.to_s }
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFunction
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/web_function.rb CHANGED
@@ -5,7 +5,9 @@ require "json"
5
5
  require "uri"
6
6
 
7
7
  require_relative "web_function/version"
8
+ require_relative "web_function/request"
8
9
  require_relative "web_function/client"
10
+ require_relative "web_function/flaggable"
9
11
  require_relative "web_function/package"
10
12
  require_relative "web_function/endpoint"
11
13
  require_relative "web_function/argument"
@@ -13,27 +15,35 @@ require_relative "web_function/attribute"
13
15
  require_relative "web_function/documented_error"
14
16
  require_relative "web_function/pipeline"
15
17
  require_relative "web_function/promise"
18
+ require_relative "web_function/utils"
16
19
 
17
20
  module WebFunction
21
+ # A base error class for WebFunction. All errors inherit from this class.
22
+ #
23
+ # @api private
24
+ #
18
25
  class Error < StandardError
19
26
  attr_reader :code, :details
20
27
 
21
- def initialize(message = nil, code: self.class.name, details: nil)
28
+ def initialize(message = nil, code: self.class.error_code, details: nil)
22
29
  super(message)
23
30
  @code = code
24
31
  @details = details
25
32
  end
26
- end
27
-
28
- class UnresolvedPromiseError < Error
29
- end
30
33
 
31
- class UnexpectedStatusCodeError < Error
32
- end
33
-
34
- class JsonParseError < Error
34
+ # Returns the error code for the error. Used as default error code if no code is provided.
35
+ #
36
+ # @return [String] The error code
37
+ #
38
+ def self.error_code
39
+ name.sub(/^WebFunction::/, "WFN_")
40
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
41
+ .upcase
42
+ end
35
43
  end
36
44
 
37
- class BadRequestError < Error
38
- end
45
+ UnresolvedPromiseError = Class.new(Error)
46
+ UnexpectedStatusCodeError = Class.new(Error)
47
+ JsonParseError = Class.new(Error)
48
+ BadRequestError = Class.new(Error)
39
49
  end
data/test.rb ADDED
@@ -0,0 +1,43 @@
1
+ require "web_function"
2
+ require "base64"
3
+
4
+ pipeline = WebFunction::Pipeline.new("http://localhost:55001/api/process-pipeline")
5
+
6
+ sdk = WebFunction::Client.from_package_endpoint("http://localhost:55001/api/sdk",
7
+ pipeline: pipeline,
8
+ )
9
+
10
+ merchant = WebFunction::Client.from_package_endpoint("http://localhost:55001/api/merchants",
11
+ bearer_auth: "reservepay_u6BHU4diPq7MVZCUJu7Ppu81nTrfYP1fMYVS",
12
+ pipeline: pipeline,
13
+ )
14
+
15
+ installations = merchant.list_installations
16
+ payment_session_id = sdk.select_payment_method(
17
+ merchant_id: "11",
18
+ installation_id: installations[0]["installation_id"],
19
+ amount: 10000,
20
+ currency: "THB",
21
+ payment_method: "PROMPTPAY",
22
+ )
23
+ payment_id = merchant.initiate_payment_flow(
24
+ amount: 10000,
25
+ currency: "THB",
26
+ capture: true,
27
+ return_url: "https://reservepay.com/",
28
+ payment_session_id: payment_session_id,
29
+ )
30
+ payment = merchant.find_payment(payment_id: payment_id)
31
+
32
+ p payment.resolve
33
+ p payment_session_id
34
+
35
+ sleep 1
36
+
37
+ qr_data = sdk.retrieve_qr_data(
38
+ merchant_id: "11",
39
+ installation_id: installations[0]["installation_id"],
40
+ payment_session_id: payment_session_id,
41
+ )
42
+
43
+ p Base64.decode64(qr_data.resolve)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_function
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Clart
@@ -60,11 +60,15 @@ files:
60
60
  - lib/web_function/client.rb
61
61
  - lib/web_function/documented_error.rb
62
62
  - lib/web_function/endpoint.rb
63
+ - lib/web_function/flaggable.rb
63
64
  - lib/web_function/package.rb
64
65
  - lib/web_function/pipeline.rb
65
66
  - lib/web_function/promise.rb
67
+ - lib/web_function/request.rb
68
+ - lib/web_function/utils.rb
66
69
  - lib/web_function/version.rb
67
70
  - sig/web_function.rbs
71
+ - test.rb
68
72
  homepage: https://github.com/robinclart/web_function
69
73
  licenses:
70
74
  - MIT