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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9624f3662fe7646de50ba70ca2606d3ff468e242c4ddfd6c9aaafff3bf249065
4
- data.tar.gz: 76e2f1871a5ca28140b659561c6eae2ae8f54d29a13ae0d71a6ff06c10c0c069
3
+ metadata.gz: e87c54742bdb4c00801a6ae3fa7d138f2be9d9ac29684bc26a9b446acd95c57c
4
+ data.tar.gz: 9f87ff03c217d5f3c5f2404777c4e80fae0ab6d99acc6ef31e63af7cc588d05a
5
5
  SHA512:
6
- metadata.gz: ef7bf7ea4b23440e81dab6487ba8e39405b45d971fdf83f7493bc575d66ff16280f31315febcbb9257303f8645d3d0761bebb1c2e21f392d80193c7d0ba3011a
7
- data.tar.gz: 734e615c1f71ac9aa3e46061b2c8703b787f2c812004b41ffc3a0e10df14e74ac50bae48ec575ce1f5351b49f080813a56bb30a02323bed5557206d8462d8ad1
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 a RFC 3986 compliant (.*)$/) do |query_string|
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 "requester"
8
- require_relative "follower"
7
+ require_relative "configurator"
9
8
  require_relative "error_handling"
10
- require_relative "status_codes"
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 = "http://localhost:2112", headers: {})
21
- @base_url = base_url
22
- configure
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] headers
44
+ # @param [Array[String] header_keys
45
45
  def remove_headers(header_keys)
46
- header_keys.map { |key| @headers.delete(key) }
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
- http = HTTPX.with(headers: request_headers)
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
- def configure
74
- @headers = CICPHash.new.merge!({
75
- "Authorization" => "Basic cm9vdDpwYXNzd29yZA==",
76
- "Content-type" => "application/json",
77
- "Accept" => "application/hal+json, application/json;q=0.9, */*;q=0.8"
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
- @parameters_list_style = "repeat_key"
81
- @etag_field = "_etag"
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
@@ -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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Halchemy
4
- VERSION = "1.0.2"
4
+ VERSION = "1.0.4"
5
5
  end
@@ -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
@@ -37,7 +37,11 @@ module Halchemy
37
37
 
38
38
  def build_url: -> String
39
39
 
40
- def configure: -> void
40
+ def configure_base: -> void
41
+
42
+ def configure_error_handling: -> void
43
+
44
+ def configure_headers: -> void
41
45
 
42
46
  def do_settings_include_status_code: -> bool
43
47
 
@@ -1,5 +1,7 @@
1
1
  module Halchemy
2
2
  class BaseRequester
3
+ LIST_STYLE_HANDLERS: Hash[String, Object]
4
+
3
5
  @_data: Object | nil
4
6
  @_headers: CICPHash
5
7
  @_is_templated: bool
data/test.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ cucumber ../../features/ -r __tests__/ --publish-quiet
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.2
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-01-26 00:00:00.000000000 Z
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
@@ -1 +0,0 @@
1
- LIST_STYLE_HANDLERS: Hash[String, Object]