halchemy 1.0.2 → 1.0.4
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/.idea/halchemy.iml +1 -0
- data/__tests__/make_http_requests/http_response_details.rb +24 -0
- data/__tests__/make_http_requests/with_parameters.rb +1 -1
- data/lib/halchemy/api.rb +31 -28
- data/lib/halchemy/configurator.rb +95 -0
- data/lib/halchemy/requester.rb +12 -12
- data/lib/halchemy/version.rb +1 -1
- data/sig/configurator.rbs +23 -0
- data/sig/halchemy/api.rbs +5 -1
- data/sig/halchemy/base_requester.rbs +2 -0
- data/test.sh +3 -0
- metadata +5 -3
- data/sig/list_style_handlers.rbs +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e87c54742bdb4c00801a6ae3fa7d138f2be9d9ac29684bc26a9b446acd95c57c
|
4
|
+
data.tar.gz: 9f87ff03c217d5f3c5f2404777c4e80fae0ab6d99acc6ef31e63af7cc588d05a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5618d243df3795345ca9957fe31f9ebb20257e15193ff85b999fe45e9b8ab0d0b43ed336021e0e7b1c3fba3e9eeac519d757d10c65921ee6ac5d142d62c7d17
|
7
|
+
data.tar.gz: 0c119ff94a610604d861bfd6d5a9e1e44842c3e31c7c43482bf89cd651d302ddcf3e9f163001cf0e9b76712407d00be5749b702bae94230a22ef1777c789493a
|
data/.idea/halchemy.iml
CHANGED
@@ -35,6 +35,7 @@
|
|
35
35
|
<orderEntry type="library" scope="PROVIDED" name="http-2 (v1.0.2, RVM: ruby-3.4.1) [gem]" level="application" />
|
36
36
|
<orderEntry type="library" scope="PROVIDED" name="http_status_codes (v0.1.0, RVM: ruby-3.4.1) [gem]" level="application" />
|
37
37
|
<orderEntry type="library" scope="PROVIDED" name="httpx (v1.4.0, RVM: ruby-3.4.1) [gem]" level="application" />
|
38
|
+
<orderEntry type="library" scope="PROVIDED" name="iniparse (v1.5.0, RVM: ruby-3.4.1) [gem]" level="application" />
|
38
39
|
<orderEntry type="library" scope="PROVIDED" name="json (v2.9.1, RVM: ruby-3.4.1) [gem]" level="application" />
|
39
40
|
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, RVM: ruby-3.4.1) [gem]" level="application" />
|
40
41
|
<orderEntry type="library" scope="PROVIDED" name="logger (v1.6.5, RVM: ruby-3.4.1) [gem]" level="application" />
|
@@ -58,3 +58,27 @@ Then(/^I can access the error details$/) do
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
When(/^I make a request that returns JSON in the body$/) do
|
63
|
+
response_body = {
|
64
|
+
"_status" => "ERR",
|
65
|
+
"_error" => {
|
66
|
+
"code" => 404,
|
67
|
+
"message" => "The requested URL was not found on the server. " /
|
68
|
+
"If you entered the URL manually please check your spelling and try again."
|
69
|
+
}
|
70
|
+
}.to_json
|
71
|
+
stub_request(:any, /.*/).to_return(body: response_body, status: 404)
|
72
|
+
|
73
|
+
@resource = {}
|
74
|
+
ALL_METHODS.each do |method|
|
75
|
+
@resource[method] = @api.follow(@root_resource).to("resource1").public_send(method)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Then(/^I can use the body in the way my language supports JSON$/) do
|
80
|
+
ALL_METHODS.each do |method|
|
81
|
+
response = @resource[method]._halchemy.response
|
82
|
+
expect(response.body.respond_to?("to_json")).to be true
|
83
|
+
end
|
84
|
+
end
|
@@ -5,7 +5,7 @@ When(/^I supply (.*)$/) do |parameters|
|
|
5
5
|
@requests = make_requests(ALL_METHODS, requester)
|
6
6
|
end
|
7
7
|
|
8
|
-
Then(/^the parameters are added to the URL as
|
8
|
+
Then(/^the parameters are added to the URL as an RFC 3986 compliant (.*)$/) do |query_string|
|
9
9
|
ALL_METHODS.each do |method|
|
10
10
|
request_path = normalize_path(@requests[method].uri.to_s)
|
11
11
|
correct_query_string = normalize_path("/path?#{query_string}")[5..]
|
data/lib/halchemy/api.rb
CHANGED
@@ -4,29 +4,31 @@ require "cicphash"
|
|
4
4
|
require "httpx"
|
5
5
|
require "http_status_codes"
|
6
6
|
|
7
|
-
require_relative "
|
8
|
-
require_relative "follower"
|
7
|
+
require_relative "configurator"
|
9
8
|
require_relative "error_handling"
|
10
|
-
require_relative "
|
11
|
-
require_relative "resource"
|
9
|
+
require_relative "follower"
|
12
10
|
require_relative "http_model"
|
13
11
|
require_relative "metadata"
|
12
|
+
require_relative "requester"
|
13
|
+
require_relative "status_codes"
|
14
|
+
require_relative "resource"
|
14
15
|
|
15
16
|
module Halchemy
|
16
17
|
# This is the Halchemy::Api class, that is the main class for interacting with HAL-based APIs
|
17
18
|
class Api
|
18
19
|
attr_accessor :base_url, :headers, :error_handling, :parameters_list_style
|
19
20
|
|
20
|
-
def initialize(base_url =
|
21
|
-
|
22
|
-
|
21
|
+
def initialize(base_url = nil, headers: {})
|
22
|
+
config = Configurator.new.config
|
23
|
+
configure_base(config)
|
24
|
+
configure_headers(config)
|
25
|
+
configure_error_handling(config)
|
23
26
|
|
27
|
+
@base_url = base_url unless base_url.nil?
|
24
28
|
@headers.merge!(headers)
|
25
29
|
end
|
26
30
|
|
27
|
-
def root
|
28
|
-
using_endpoint("/", is_root: true)
|
29
|
-
end
|
31
|
+
def root = using_endpoint("/", is_root: true)
|
30
32
|
|
31
33
|
def using_endpoint(target, is_root: false)
|
32
34
|
if is_root
|
@@ -37,13 +39,11 @@ module Halchemy
|
|
37
39
|
end
|
38
40
|
|
39
41
|
# @param [Hash[String, string]] headers
|
40
|
-
def add_headers(headers)
|
41
|
-
@headers.merge!(headers)
|
42
|
-
end
|
42
|
+
def add_headers(headers) = @headers.merge!(headers)
|
43
43
|
|
44
|
-
# @param [Array[String]
|
44
|
+
# @param [Array[String] header_keys
|
45
45
|
def remove_headers(header_keys)
|
46
|
-
header_keys.
|
46
|
+
header_keys.each { |key| @headers.delete(key) }
|
47
47
|
end
|
48
48
|
|
49
49
|
def request(method, target, headers = nil, data = nil)
|
@@ -52,16 +52,13 @@ module Halchemy
|
|
52
52
|
request_headers = headers.nil? ? @headers : @headers.merge(headers)
|
53
53
|
request = HttpModel::Request.new(method, url, data, request_headers)
|
54
54
|
|
55
|
-
|
56
|
-
result = http.public_send(method, url, body: data)
|
55
|
+
result = HTTPX.with(headers: request_headers).send(method, url, body: data)
|
57
56
|
|
58
57
|
raise_for_errors(result)
|
59
58
|
build_resource(request, result)
|
60
59
|
end
|
61
60
|
|
62
|
-
def follow(resource)
|
63
|
-
Follower.new(self, resource)
|
64
|
-
end
|
61
|
+
def follow(resource) = Follower.new(self, resource)
|
65
62
|
|
66
63
|
def optimistic_concurrency_header(resource)
|
67
64
|
etag = resource._halchemy.response.headers["Etag"] || resource[@etag_field]
|
@@ -70,15 +67,21 @@ module Halchemy
|
|
70
67
|
|
71
68
|
private
|
72
69
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
70
|
+
# @return [void]
|
71
|
+
def configure_headers(config)
|
72
|
+
@headers = CICPHash.new.merge!(config["headers"])
|
73
|
+
end
|
74
|
+
|
75
|
+
def configure_error_handling(config)
|
79
76
|
@error_handling = ErrorHandling.new
|
80
|
-
@
|
81
|
-
@
|
77
|
+
@error_handling.raise_for_status_codes = config["error_handling"]["raise_for_status_codes"]
|
78
|
+
@error_handling.raise_for_network_errors = config["error_handling"]["raise_for_network_errors"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def configure_base(config)
|
82
|
+
@base_url = config["halchemy"]["base_url"] if @base_url.nil?
|
83
|
+
@parameters_list_style = config["halchemy"]["parameters_list_style"]
|
84
|
+
@etag_field = config["halchemy"]["etag_field"]
|
82
85
|
end
|
83
86
|
|
84
87
|
def build_url(target)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "iniparse"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
# provides default configuration values, which can be overridden by a config file in the home directory
|
7
|
+
# or the project directory
|
8
|
+
class Configurator
|
9
|
+
DEFAULT_CONFIG = {
|
10
|
+
"halchemy" => {
|
11
|
+
"base_url" => "http://localhost:2112",
|
12
|
+
"parameters_list_style" => "repeat_key",
|
13
|
+
"etag_field" => "_etag"
|
14
|
+
},
|
15
|
+
"headers" => {
|
16
|
+
"Content-type" => "application/json",
|
17
|
+
"Accept" => "application/hal+json, application/json;q=0.9, */*;q=0.8",
|
18
|
+
"Authorization" => "Basic cm9vdDpwYXNzd29yZA==" # root:password
|
19
|
+
},
|
20
|
+
"error_handling" => {
|
21
|
+
"raise_for_network_errors" => true,
|
22
|
+
"raise_for_status_codes" => nil
|
23
|
+
}
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :config
|
27
|
+
|
28
|
+
def initialize(filename = ".halchemy")
|
29
|
+
@filename = filename
|
30
|
+
@config = DEFAULT_CONFIG.dup
|
31
|
+
load_config
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Find the project root by searching for known indicators
|
37
|
+
def find_project_root(current_dir = nil)
|
38
|
+
current_dir ||= determine_current_dir
|
39
|
+
return nil unless current_dir
|
40
|
+
|
41
|
+
root_indicators = root_indicator_files
|
42
|
+
|
43
|
+
traverse_to_root(current_dir, root_indicators)
|
44
|
+
end
|
45
|
+
|
46
|
+
def determine_current_dir
|
47
|
+
caller_location = caller(1..1)&.first
|
48
|
+
file_path = caller_location&.split(":")&.first
|
49
|
+
file_path ? File.expand_path(File.dirname(file_path)) : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def root_indicator_files
|
53
|
+
%w[.git Gemfile Gemfile.lock config.ru Rakefile .project .idea .vscode .halchemy]
|
54
|
+
end
|
55
|
+
|
56
|
+
def traverse_to_root(current_dir, root_indicators)
|
57
|
+
while current_dir != File.dirname(current_dir) # Stop at filesystem root
|
58
|
+
return current_dir if root_indicators.any? { |indicator| File.exist?(File.join(current_dir, indicator)) }
|
59
|
+
|
60
|
+
current_dir = File.dirname(current_dir)
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parse an INI file into a nested hash
|
66
|
+
def ini_to_hash(file_path)
|
67
|
+
return {} unless File.exist?(file_path)
|
68
|
+
|
69
|
+
IniParse.parse(File.read(file_path)).to_h
|
70
|
+
rescue StandardError => e
|
71
|
+
warn "Failed to parse INI file: #{file_path}\nError: #{e.message}"
|
72
|
+
{}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Load configuration from both the project root and home directory
|
76
|
+
def load_config
|
77
|
+
home_config = ini_to_hash(File.join(Dir.home, @filename))
|
78
|
+
merge_config(home_config)
|
79
|
+
|
80
|
+
project_root = find_project_root
|
81
|
+
return unless project_root
|
82
|
+
|
83
|
+
project_config = ini_to_hash(File.join(project_root, @filename))
|
84
|
+
merge_config(project_config)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Deep merge configurations
|
88
|
+
def merge_config(new_config)
|
89
|
+
@config.each_key do |section|
|
90
|
+
next unless new_config[section]
|
91
|
+
|
92
|
+
@config[section].merge!(new_config[section])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/halchemy/requester.rb
CHANGED
@@ -2,22 +2,22 @@
|
|
2
2
|
|
3
3
|
require "uri_template"
|
4
4
|
|
5
|
-
LIST_STYLE_HANDLERS = {
|
6
|
-
"repeat_key" => ->(key, array) { array.map { |item| [key, URI.encode_www_form_component(item.to_s)] } },
|
7
|
-
"bracket" => ->(key, array) { array.map { |item| ["#{key}[]", URI.encode_www_form_component(item.to_s)] } },
|
8
|
-
"index" => lambda { |key, array|
|
9
|
-
array.each_with_index.map do |item, index|
|
10
|
-
["#{key}[#{index}]", URI.encode_www_form_component(item.to_s)]
|
11
|
-
end
|
12
|
-
},
|
13
|
-
"comma" => ->(key, array) { [[key, array.map { |item| URI.encode_www_form_component(item.to_s) }.join(",")]] },
|
14
|
-
"pipe" => ->(key, array) { [[key, array.map { |item| URI.encode_www_form_component(item.to_s) }.join("|")]] }
|
15
|
-
}.freeze
|
16
|
-
|
17
5
|
module Halchemy
|
18
6
|
# The results of a Follower#to is a Requester. In the case of a GET for the root resource, the Requester is Read Only
|
19
7
|
# Otherwise it is a full Requester. Both requester types share much in common. This is defined in BaseRequester
|
20
8
|
class BaseRequester
|
9
|
+
LIST_STYLE_HANDLERS = {
|
10
|
+
"repeat_key" => ->(key, array) { array.map { |item| [key, URI.encode_www_form_component(item.to_s)] } },
|
11
|
+
"bracket" => ->(key, array) { array.map { |item| ["#{key}[]", URI.encode_www_form_component(item.to_s)] } },
|
12
|
+
"index" => lambda { |key, array|
|
13
|
+
array.each_with_index.map do |item, index|
|
14
|
+
["#{key}[#{index}]", URI.encode_www_form_component(item.to_s)]
|
15
|
+
end
|
16
|
+
},
|
17
|
+
"comma" => ->(key, array) { [[key, array.map { |item| URI.encode_www_form_component(item.to_s) }.join(",")]] },
|
18
|
+
"pipe" => ->(key, array) { [[key, array.map { |item| URI.encode_www_form_component(item.to_s) }.join("|")]] }
|
19
|
+
}.freeze
|
20
|
+
|
21
21
|
# @param [Halchemy::Api] api
|
22
22
|
# @param [String | Tuple[HalResource, String]] target
|
23
23
|
# @return [void]
|
data/lib/halchemy/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
class Configurator
|
2
|
+
DEFAULT_CONFIG: Hash[String, Object]
|
3
|
+
|
4
|
+
@filename: String
|
5
|
+
|
6
|
+
attr_reader config: Hash[String, Object]
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def determine_current_dir: -> (String | nil)
|
11
|
+
|
12
|
+
def find_project_root: -> void
|
13
|
+
|
14
|
+
def ini_to_hash: -> Hash[String, Object]
|
15
|
+
|
16
|
+
def load_config: -> Hash[String, Object]
|
17
|
+
|
18
|
+
def merge_config: -> void
|
19
|
+
|
20
|
+
def root_indicator_files: -> Array[String]
|
21
|
+
|
22
|
+
def traverse_to_root: -> void
|
23
|
+
end
|
data/sig/halchemy/api.rbs
CHANGED
data/test.sh
ADDED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: halchemy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Ottoson
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: Do you have an API that serves data following the HAL specification? The
|
13
13
|
**halchemy** library makes it easy for your client to make the most of that API.
|
@@ -42,6 +42,7 @@ files:
|
|
42
42
|
- bdd
|
43
43
|
- lib/halchemy.rb
|
44
44
|
- lib/halchemy/api.rb
|
45
|
+
- lib/halchemy/configurator.rb
|
45
46
|
- lib/halchemy/error_handling.rb
|
46
47
|
- lib/halchemy/follower.rb
|
47
48
|
- lib/halchemy/http_model.rb
|
@@ -54,6 +55,7 @@ files:
|
|
54
55
|
- sig/__tests__/halchemy.rbs
|
55
56
|
- sig/__tests__/headers.rbs
|
56
57
|
- sig/__tests__/remove_headers.rbs
|
58
|
+
- sig/configurator.rbs
|
57
59
|
- sig/halchemy/api.rbs
|
58
60
|
- sig/halchemy/base_requester.rbs
|
59
61
|
- sig/halchemy/error_handling.rbs
|
@@ -65,9 +67,9 @@ files:
|
|
65
67
|
- sig/halchemy/read_only_requester.rbs
|
66
68
|
- sig/halchemy/requester.rbs
|
67
69
|
- sig/halchemy/resource.rbs
|
68
|
-
- sig/list_style_handlers.rbs
|
69
70
|
- sig/matchers.rbs
|
70
71
|
- sig/patterns.rbs
|
72
|
+
- test.sh
|
71
73
|
homepage: https://github.com/pointw-dev/halchemy
|
72
74
|
licenses:
|
73
75
|
- MIT
|
data/sig/list_style_handlers.rbs
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
LIST_STYLE_HANDLERS: Hash[String, Object]
|