lutaml-hal 0.1.3 → 0.1.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: da904351e361ae9657c850d5faf981c6b698583970ead3320f125fdf6078fcbb
4
- data.tar.gz: 677a8d713e5c2499cd9250497ca2b6f2d9474e545a2d2b2989dbcd33802b5588
3
+ metadata.gz: b1006ba9c57ff945fa51c368b1ca12971d83fc47f668513a962da76811b6cc91
4
+ data.tar.gz: 4bc4bc6697181d9512604736ba42a6d4ce16d67d235ffd5ef79b481e09ca8243
5
5
  SHA512:
6
- metadata.gz: d1d55faaa7c1f7d45b07a3d475b6908de807f7bed3bfebdfa32ce0ada2b91a1794f948046e0e0c554216f5e18219c07ee81dfb03a2ee0f5fb19e3367821e40fb
7
- data.tar.gz: '07085ed8d705dfe5ff1893e1c9d2ab7bc87fe3b22e483dc14edfb1b89b2e54942e21fa38f6c1ff7910030fb57c6ecf198110fa70ef8447a49c377de2935873d8'
6
+ metadata.gz: befafec41b21cd9244ae937bdb5ce5e975a99bd708cd960e68b4db122554b73d73ef506bc82bda0961d196f10785957c10246d14607345d2e8239420571b1a58
7
+ data.tar.gz: e719a907ff8e198f797691829b4fab1280e866f8a403ca80945a55f0178d48a0737c274dc12f3052c635d96f53d7ca3663081f12359293638aa7d6b2d45f9845
data/README.adoc CHANGED
@@ -71,6 +71,10 @@ A registry for managing HAL resource models and their endpoints. It allows you
71
71
  to register models, define their relationships, and fetch resources from the
72
72
  API.
73
73
 
74
+ `Lutaml::Hal::GlobalRegister`::
75
+ A global registry (Singleton) for managing ModelRegisters and facilitating model
76
+ resolution across different resources. Its usage is optional.
77
+
74
78
  `Lutaml::Hal::Resource`::
75
79
  A base class for defining HAL resource models. It includes methods for
76
80
  defining attributes, links, and key-value mappings for resources.
@@ -96,6 +100,8 @@ At the data definition phase:
96
100
  . Define the API endpoint using the `Client` class.
97
101
  . Create a `ModelRegister` to manage the resource models and their
98
102
  respective endpoints.
103
+ . (optional) Create a `GlobalRegister` to manage one or more `ModelRegister`
104
+ instances. It is necessary for automatic Link resolution.
99
105
  . Define the resource models using the `Resource` class.
100
106
  . Register the models with the `ModelRegister` and define their
101
107
  relationships using the `add_endpoint` method.
@@ -145,10 +151,38 @@ require 'lutaml-hal'
145
151
 
146
152
  # Create a new client with API endpoint
147
153
  client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
148
- register = Lutaml::Hal::ModelRegister.new(client: client)
154
+ register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)
149
155
  # Or set client later, `register.client = client`
150
156
  ----
151
157
 
158
+ The `name:` parameter is used to identify the `ModelRegister` instance.
159
+
160
+ === Creating a HAL global register
161
+
162
+ The `GlobalRegister` class is a singleton that manages one or more
163
+ `ModelRegister` instances.
164
+
165
+ It is optional, but is required for automatic realization of models from Link
166
+ objects. See <<fetching_resource_via_link_realization>> for more details.
167
+
168
+ [source,ruby]
169
+ ----
170
+ require 'lutaml-hal'
171
+
172
+ # Create a new client with API endpoint
173
+ client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
174
+ register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)
175
+
176
+ # Register the ModelRegister with the global register
177
+ global_register = Lutaml::Hal::GlobalRegister.instance.register(:my_model_register, register)
178
+
179
+ # Obtain the global register
180
+ global_register.get(:my_model_register)
181
+
182
+ # Delete a register mapping
183
+ global_register.delete(:my_model_register)
184
+ ----
185
+
152
186
 
153
187
  === Defining HAL resource models
154
188
 
@@ -158,7 +192,7 @@ A HAL resource is defined by creating a subclass of the `Resource` class and
158
192
  defining its attributes, links, and key-value mappings.
159
193
 
160
194
  The `Resource` class is the base class for defining HAL resource models.
161
- It inherits from `Lutaml::Model::Serialization`, which provides data
195
+ It inherits from `Lutaml::Model::Serializable`, which provides data
162
196
  modelling and serialization capabilities.
163
197
 
164
198
  The declaration of attributes, links, and key-value mappings for a HAL resource
@@ -780,16 +814,34 @@ product_index
780
814
  ====
781
815
 
782
816
 
817
+ [[fetching_resource_via_link_realization]]
783
818
  === Fetching a resource via link realization
784
819
 
785
820
  Given a resource index that contains links to resources, the individual resource
786
821
  links can be "realized" as actual model instances through the
787
- `Link#realize(register)` method which dynamically retrieves the resource.
822
+ `Link#realize(register:)` method which dynamically retrieves the resource.
788
823
 
789
824
  Given a `Link` object, the `realize` method fetches the resource from the API
790
825
  using the provided `register`.
791
826
 
792
- Syntax:
827
+ There are two ways a resource gets realized from a `Link` object:
828
+
829
+ * If a `Lutaml::Hal::GlobalRegister` is used, and the `Link` object originated
830
+ from a fetch using a `ModelRegister` then the `realize` method has sufficient
831
+ information to automatically fetch the resource from the API using the same
832
+ `register`.
833
+ +
834
+ NOTE: This relies on the `Hal::REGISTER_ID_ATTR_NAME` attribute to be set
835
+ in the `ModelRegister` class. This attribute is used to identify the
836
+ resource endpoint ID in the URL.
837
+
838
+ * If a `GlobalRegister` is not used, even if the Link object originated
839
+ from a fetch using a `ModelRegister`, the `realize` method does not have sufficient
840
+ information to fetch the resource from the API using the same
841
+ `register`. In this case an explicit `register` must be provided to the
842
+ `realize(register: ...)` method.
843
+
844
+ Syntax for standalone usage:
793
845
 
794
846
  [source,ruby]
795
847
  ----
@@ -813,12 +865,26 @@ NOTE: It is possible to use the `realize` method on a link object using another
813
865
  `ModelRegister` instance. This is useful when you want to resolve a link
814
866
  using a different API endpoint or a different set of resource models.
815
867
 
868
+ Syntax when using a `GlobalRegister`:
869
+
870
+ [source,ruby]
871
+ ----
872
+ resource_index = model_register.fetch(:resource_index)
873
+ resource_index.links.products.first.realize
874
+ # => client.get('/resources/1')
875
+ ----
876
+
816
877
  .Dynamically realizing a resource from the collection using links
817
878
  [example]
818
879
  ====
819
880
  [source,ruby]
820
881
  ----
882
+ # Without a GlobalRegister
821
883
  product_2 = product_index.links.products.last.realize(register)
884
+
885
+ # With a GlobalRegister
886
+ product_2 = product_index.links.products.last.realize
887
+
822
888
  # => client.get('/products/2')
823
889
  # => {
824
890
  # "id": 2,
@@ -842,9 +908,16 @@ product_2
842
908
  # <ProductLink href: "/products/4", title: "Product 4">,
843
909
  # <ProductLink href: "/products/6", title: "Product 6">
844
910
  # ]}>
911
+
912
+ # Without a GlobalRegister
913
+ product_2_related_1 = product_2.links.related.first.realize(register)
914
+
915
+ # With a GlobalRegister
916
+ product_2_related_1 = product_2.links.related.first.realize
845
917
  ----
846
918
  ====
847
919
 
920
+
848
921
  === Handling HAL pages / pagination
849
922
 
850
923
  The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
@@ -907,7 +980,12 @@ page_1
907
980
  # next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
908
981
  # last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
909
982
 
983
+ # Without a GlobalRegister
910
984
  page_2 = page.links.next.realize(register)
985
+
986
+ # With a GlobalRegister
987
+ page_2 = page.links.next.realize
988
+
911
989
  # => client.get('/resources?page=2&items=10')
912
990
  # => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
913
991
  # links: #<ResourceIndexLinks
@@ -19,12 +19,19 @@ module Lutaml
19
19
  @debug = options[:debug] || !ENV['DEBUG_API'].nil?
20
20
  @cache = options[:cache] || {}
21
21
  @cache_enabled = options[:cache_enabled] || false
22
+
23
+ @api_url = strip_api_url(@api_url)
24
+ end
25
+
26
+ # Strip any trailing slash from the API URL
27
+ def strip_api_url(url)
28
+ url.sub(%r{/\Z}, '')
22
29
  end
23
30
 
24
31
  # Get a resource by its full URL
25
32
  def get_by_url(url, params = {})
26
33
  # Strip API endpoint if it's included
27
- path = url.sub(%r{^#{@api_url}/}, '')
34
+ path = strip_api_url(url)
28
35
  get(path, params)
29
36
  end
30
37
 
@@ -62,7 +69,7 @@ module Lutaml
62
69
  end
63
70
 
64
71
  def handle_response(response, url)
65
- debug_log(response, url) if @debug
72
+ debug_api_log(response, url) if @debug
66
73
 
67
74
  case response.status
68
75
  when 200..299
@@ -80,11 +87,11 @@ module Lutaml
80
87
  end
81
88
  end
82
89
 
83
- def debug_log(response, url)
90
+ def debug_api_log(response, url)
84
91
  if defined?(Rainbow)
85
- puts Rainbow("\n===== DEBUG: HAL API REQUEST =====").blue
92
+ puts Rainbow("\n===== Lutaml::Hal DEBUG: HAL API REQUEST =====").blue
86
93
  else
87
- puts "\n===== DEBUG: HAL API REQUEST ====="
94
+ puts "\n===== Lutaml::Hal DEBUG: HAL API REQUEST ====="
88
95
  end
89
96
 
90
97
  puts "URL: #{url}"
@@ -8,5 +8,6 @@ module Lutaml
8
8
  class BadRequestError < Error; end
9
9
  class ServerError < Error; end
10
10
  class LinkResolutionError < Error; end
11
+ class ParsingError < Error; end
11
12
  end
12
13
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Lutaml
6
+ module Hal
7
+ # Global register for model registers
8
+ # This class is a singleton that manages the registration and retrieval of model registers.
9
+ # It ensures that each model register is unique and provides a way to access them globally.
10
+ #
11
+ # @example
12
+ # global_register = GlobalRegister.instance
13
+ # global_register.register(:example, ExampleModelRegister.new)
14
+ # example_register = global_register.get(:example)
15
+ class GlobalRegister
16
+ include Singleton
17
+
18
+ def initialize
19
+ @model_registers = {}
20
+ end
21
+
22
+ def register(name, model_register)
23
+ if @model_registers[name] && @model_registers[name] != model_register
24
+ raise "Model register with name #{name} replacing another one" \
25
+ " (#{@model_registers[name].inspect} vs #{model_register.inspect})"
26
+ end
27
+
28
+ @model_registers[name] = model_register
29
+ end
30
+
31
+ def get(name)
32
+ raise "Model register with name #{name} not found" unless @model_registers[name]
33
+
34
+ @model_registers[name]
35
+ end
36
+
37
+ def delete(name)
38
+ return unless @model_registers[name]
39
+
40
+ @model_registers.delete(name)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -7,6 +7,10 @@ module Lutaml
7
7
  module Hal
8
8
  # HAL Link representation with realization capability
9
9
  class Link < Lutaml::Model::Serializable
10
+ # This is the model register that has fetched the origin of this link, and
11
+ # will be used to resolve unless overriden in resource#realize()
12
+ attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
13
+
10
14
  attribute :href, :string
11
15
  attribute :title, :string
12
16
  attribute :name, :string
@@ -16,9 +20,33 @@ module Lutaml
16
20
  attribute :profile, :string
17
21
  attribute :lang, :string
18
22
 
19
- # Fetch the actual resource this link points to
20
- def realize(register)
21
- register.resolve_and_cast(href)
23
+ # Fetch the actual resource this link points to.
24
+ # This method will use the global register according to the source of the Link object.
25
+ # If the Link does not have a register, a register needs to be provided explicitly
26
+ # via the `register:` parameter.
27
+ def realize(register: nil)
28
+ register = find_register(register)
29
+ raise "No register provided for link resolution (class: #{self.class}, href: #{href})" if register.nil?
30
+
31
+ Hal.debug_log "Resolving link href: #{href} using register"
32
+ register.resolve_and_cast(self, href)
33
+ end
34
+
35
+ private
36
+
37
+ def find_register(explicit_register)
38
+ return explicit_register if explicit_register
39
+
40
+ register_id = instance_variable_get("@#{Hal::REGISTER_ID_ATTR_NAME}")
41
+ return nil if register_id.nil?
42
+
43
+ register = Lutaml::Hal::GlobalRegister.instance.get(register_id)
44
+ if register.nil?
45
+ raise 'GlobalRegister in use but unable to find the register. '\
46
+ 'Please provide a register to the `#realize` method to resolve the link'
47
+ end
48
+
49
+ register
22
50
  end
23
51
  end
24
52
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'model_register'
5
+
6
+ module Lutaml
7
+ module Hal
8
+ # HAL Link representation with realization capability
9
+ class LinkSet < Lutaml::Model::Serializable
10
+ attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
11
+ end
12
+ end
13
+ end
@@ -6,9 +6,10 @@ module Lutaml
6
6
  module Hal
7
7
  # Register to map URL patterns to model classes
8
8
  class ModelRegister
9
- attr_accessor :models, :client
9
+ attr_accessor :models, :client, :register_name
10
10
 
11
- def initialize(client: nil)
11
+ def initialize(name:, client: nil)
12
+ @register_name = name
12
13
  # If `client` is not set, it can be set later
13
14
  @client = client
14
15
  @models = {}
@@ -39,27 +40,59 @@ module Lutaml
39
40
  url = interpolate_url(endpoint[:url], params)
40
41
  response = client.get(url)
41
42
 
42
- endpoint[:model].from_json(response.to_json)
43
+ realized_model = endpoint[:model].from_json(response.to_json)
44
+
45
+ mark_model_links_with_register(realized_model)
46
+ realized_model
43
47
  end
44
48
 
45
- def resolve_and_cast(href)
49
+ def resolve_and_cast(link, href)
46
50
  raise 'Client not configured' unless client
47
51
 
48
- debug_log("href #{href}")
52
+ Hal.debug_log("resolve_and_cast: link #{link}, href #{href}")
49
53
  response = client.get_by_url(href)
50
54
 
51
- # TODO: Merge more content into the resource
55
+ # TODO: Merge full Link content into the resource?
52
56
  response_with_link_details = response.to_h.merge({ 'href' => href })
53
57
 
54
58
  href_path = href.sub(client.api_url, '')
59
+
55
60
  model_class = find_matching_model_class(href_path)
56
61
  raise LinkResolutionError, "Unregistered URL pattern: #{href}" unless model_class
57
62
 
58
- debug_log("model_class #{model_class}")
59
- debug_log("response: #{response.inspect}")
60
- debug_log("amended: #{response_with_link_details}")
63
+ Hal.debug_log("resolve_and_cast: resolved to model_class #{model_class}")
64
+ Hal.debug_log("resolve_and_cast: response: #{response.inspect}")
65
+ Hal.debug_log("resolve_and_cast: amended: #{response_with_link_details}")
61
66
 
62
- model_class.from_json(response_with_link_details.to_json)
67
+ model = model_class.from_json(response_with_link_details.to_json)
68
+ mark_model_links_with_register(model)
69
+ model
70
+ end
71
+
72
+ # Recursively mark all models in the link with the register name
73
+ # This is used to ensure that all links in the model are registered
74
+ # with the same register name for consistent resolution
75
+ def mark_model_links_with_register(inspecting_model)
76
+ return unless inspecting_model.is_a?(Lutaml::Model::Serializable)
77
+
78
+ inspecting_model.instance_variable_set("@#{Hal::REGISTER_ID_ATTR_NAME}", @register_name)
79
+
80
+ # Recursively process model attributes to mark links with this register
81
+ inspecting_model.class.attributes.each_pair do |key, config|
82
+ attr_type = config.type
83
+ next unless attr_type < Lutaml::Hal::Resource ||
84
+ attr_type < Lutaml::Hal::Link ||
85
+ attr_type < Lutaml::Hal::LinkSet
86
+
87
+ value = inspecting_model.send(key)
88
+ next if value.nil?
89
+
90
+ # Handle both array and single values with the same logic
91
+ values = value.is_a?(Array) ? value : [value]
92
+ values.each { |item| mark_model_links_with_register(item) }
93
+ end
94
+
95
+ inspecting_model
63
96
  end
64
97
 
65
98
  private
@@ -103,11 +136,15 @@ module Lutaml
103
136
  pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
104
137
  # Convert * wildcards to regex pattern
105
138
  regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
106
- regex.match?(url)
107
- end
108
139
 
109
- def debug_log(message)
110
- puts "DEBUG: #{message}" if ENV['DEBUG_API']
140
+ Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
141
+ Hal.debug_log("pattern_match?: href to match #{url}")
142
+ Hal.debug_log("pattern_match?: pattern to match #{pattern_with_wildcards}")
143
+
144
+ matches = regex.match?(url)
145
+ Hal.debug_log("pattern_match?: matches = #{matches}")
146
+
147
+ matches
111
148
  end
112
149
  end
113
150
  end
@@ -23,7 +23,7 @@ module Lutaml
23
23
  def self.inherited(subclass)
24
24
  super
25
25
 
26
- page_links_symbols = %i[self next prev first last]
26
+ page_links_symbols = %i[self next prev first last up]
27
27
  subclass_name = subclass.name
28
28
  subclass.class_eval do
29
29
  # Define common page links
@@ -7,6 +7,10 @@ module Lutaml
7
7
  module Hal
8
8
  # Resource class for all HAL resources
9
9
  class Resource < Lutaml::Model::Serializable
10
+ # This is the model register that has fetched this resource, and
11
+ # will be used to resolve links unless overriden in resource#realize()
12
+ attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
13
+
10
14
  class << self
11
15
  attr_accessor :link_definitions
12
16
 
@@ -35,20 +39,22 @@ module Lutaml
35
39
  # Use the provided "key" as the attribute name
36
40
  attribute_name = attr_key.to_sym
37
41
 
42
+ Hal.debug_log "Defining HAL link for `#{attr_key}` with realize class `#{realize_class}`"
43
+
38
44
  # Create a dynamic Link subclass name based on "realize_class", the
39
45
  # class to realize for a Link object, if `link_class:` is not provided.
40
46
  link_klass = link_class || create_link_class(realize_class)
41
47
 
42
48
  # Create a dynamic LinkSet class if `link_set_class:` is not provided.
43
49
  unless link_set_class
44
- link_set_klass = link_set_class || get_links_class
50
+ link_set_klass = link_set_class || get_link_set_class
45
51
  link_set_klass.class_eval do
46
52
  # Declare the corresponding lutaml-model attribute
47
53
  attribute attribute_name, link_klass, collection: collection
48
54
 
49
55
  # Define the mapping for the attribute
50
56
  key_value do
51
- map attr_key, to: attribute_name
57
+ map key, to: attribute_name
52
58
  end
53
59
  end
54
60
  end
@@ -67,7 +73,7 @@ module Lutaml
67
73
  end
68
74
 
69
75
  # This method obtains the Links class that holds the Link classes
70
- def get_links_class
76
+ def get_link_set_class
71
77
  parent_klass_name = name.split('::')[0..-2].join('::')
72
78
  child_klass_name = "#{name.split('::').last}LinkSet"
73
79
  klass_name = [parent_klass_name, child_klass_name].join('::')
@@ -86,12 +92,14 @@ module Lutaml
86
92
  child_klass_name = "#{name.split('::').last}LinkSet"
87
93
  klass_name = [parent_klass_name, child_klass_name].join('::')
88
94
 
95
+ Hal.debug_log "Creating link set class #{klass_name}"
96
+
89
97
  # Check if the LinkSet class is already defined, return if so
90
98
  return Object.const_get(klass_name) if Object.const_defined?(klass_name)
91
99
 
92
100
  # Define the LinkSet class dynamically as a normal Lutaml::Model class
93
- # since it is not a Resource
94
- klass = Class.new(Lutaml::Model::Serializable)
101
+ # since it is not a Resource.
102
+ klass = Class.new(Lutaml::Hal::LinkSet)
95
103
  parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
96
104
  parent_klass.const_set(child_klass_name, klass)
97
105
 
@@ -111,9 +119,11 @@ module Lutaml
111
119
  # This is a Link class that helps us realize the targeted class
112
120
  def create_link_class(realize_class_name)
113
121
  parent_klass_name = name.split('::')[0..-2].join('::')
114
- child_klass_name = "#{name.split('::').last}Link"
122
+ child_klass_name = "#{realize_class_name.split('::').last}Link"
115
123
  klass_name = [parent_klass_name, child_klass_name].join('::')
116
124
 
125
+ Hal.debug_log "Creating link class #{klass_name} for #{realize_class_name}"
126
+
117
127
  return Object.const_get(klass_name) if Object.const_defined?(klass_name)
118
128
 
119
129
  # Define the link class dynamically
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.3'
5
+ VERSION = '0.1.4'
6
6
  end
7
7
  end
data/lib/lutaml/hal.rb CHANGED
@@ -5,13 +5,20 @@ require 'lutaml/model'
5
5
  module Lutaml
6
6
  # HAL implementation for Lutaml
7
7
  module Hal
8
+ REGISTER_ID_ATTR_NAME = '_global_register_id'
9
+
10
+ def self.debug_log(message)
11
+ puts "[Lutaml::Hal] DEBUG: #{message}" if ENV['DEBUG_API']
12
+ end
8
13
  end
9
14
  end
10
15
 
11
16
  require_relative 'hal/version'
12
17
  require_relative 'hal/errors'
13
18
  require_relative 'hal/link'
19
+ require_relative 'hal/link_set'
14
20
  require_relative 'hal/resource'
15
21
  require_relative 'hal/page'
22
+ require_relative 'hal/global_register'
16
23
  require_relative 'hal/model_register'
17
24
  require_relative 'hal/client'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-hal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -80,7 +80,9 @@ files:
80
80
  - lib/lutaml/hal.rb
81
81
  - lib/lutaml/hal/client.rb
82
82
  - lib/lutaml/hal/errors.rb
83
+ - lib/lutaml/hal/global_register.rb
83
84
  - lib/lutaml/hal/link.rb
85
+ - lib/lutaml/hal/link_set.rb
84
86
  - lib/lutaml/hal/model_register.rb
85
87
  - lib/lutaml/hal/page.rb
86
88
  - lib/lutaml/hal/resource.rb