sinatra_more 0.3.13 → 0.3.14

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -49,6 +49,7 @@ Here is a small list of what sinatra_more contains:
49
49
  * Generic view and tag helpers (<tt>tag</tt>, <tt>content_tag</tt>, <tt>input_tag</tt>, ...)
50
50
  * Asset tag helpers (<tt>link_to</tt>, <tt>image_tag</tt>, <tt>javascript_include_tag</tt>, ...)
51
51
  * Full form helpers and builders support (<tt>form_tag</tt>, <tt>form_for</tt>, <tt>field_set_tag</tt>, <tt>text_field</tt>, ...)
52
+ * Full url named route support to avoid hardcoding url paths in sinatra (<tt>map</tt>, <tt>url_for</tt>)
52
53
  * Generally useful formatting extensions (<tt>relative_time_ago</tt>, <tt>js_escape_html</tt>, <tt>sanitize_html</tt>)
53
54
  * Simple 'mailer' support for sinatra (akin to <tt>ActionMailer</tt> but simpler and powered by <tt>pony</tt>)
54
55
  * Plug and play setup for the excellent Warden authentication system
@@ -81,6 +82,7 @@ different components based on which pieces are useful for your particular applic
81
82
  register SinatraMore::RenderPlugin
82
83
  register SinatraMore::WardenPlugin
83
84
  register SinatraMore::MailerPlugin
85
+ register SinatraMore::RoutingPlugin
84
86
  end
85
87
 
86
88
  This will then allow you to use the components that have been registered. A breakdown of components is below:
@@ -577,6 +579,84 @@ or perhaps we want to have a short body without the need for a template file:
577
579
  end
578
580
 
579
581
  See the wiki article for additional information: <http://wiki.github.com/nesquena/sinatra_more/mailerplugin>
582
+
583
+ == RoutingPlugin
584
+
585
+ This component provides Sinatra with an enhanced url routing system which enables named route aliases to be defined
586
+ and used throughout your application to refer to urls. The benefits of this is that instead of having to hard-code route urls
587
+ into every area of your application, now we can just define the urls in a single spot and then attach an alias
588
+ which can be used to refer to the url throughout the rest.
589
+
590
+ Let's take a look at how to define named route mappings:
591
+
592
+ # /app/routes/example.rb
593
+ require 'sinatra_more'
594
+
595
+ class RoutingDemo < Sinatra::Application
596
+ register SinatraMore::RoutingPlugin
597
+
598
+ # Define the named route mappings
599
+ map(:account).to("/the/accounts/:name/and/:id")
600
+ map(:accounts).to("/the/accounts/index")
601
+
602
+ # Configure the routes using the named alias
603
+ get(:account) { "name: params[:name] - id: params[:id]" }
604
+ get(:accounts) { "I am the body for the url /the/accounts/index" }
605
+ end
606
+
607
+ Notice we simply create a route alias using the <tt>map</tt> function and then pass in the corresponding url into the <tt>to</tt> method.
608
+ You can then define the routes using the named symbol representing the url. The route aliases can be accessed using <tt>url_for</tt>
609
+
610
+ url_for(:accounts)
611
+ url_for(:account, :id => 1, :name => 'first')
612
+
613
+ You can also refer to the url in views using <tt>url_for</tt>
614
+
615
+ # /app/views/index.erb
616
+ <p>Go to the <%= link_to 'accounts dashboard', url_for(:accounts) %> to view your accounts</p>
617
+ <p>Go to account for <%= link_to 'first account', url_for(:account, :id => 1, :name => 'first') %>
618
+
619
+ Simply invoking <tt>url_for(name, *parameters)</tt> will return the full mapped url for use in links or anywhere else
620
+ that the url might be required.
621
+
622
+ The routing system also supports url route configuration namespaces:
623
+
624
+ # /app/routes/example.rb
625
+ map(:admin, :show).to("/admin/:id/show")
626
+
627
+ namespace :admin do
628
+ get :show do
629
+ "admin show for #{params[:id]}"
630
+ end
631
+ end
632
+
633
+ You could also define the route aliases themselves using a namespace for convenience:
634
+
635
+ # /app/routes/example.rb
636
+ map :admin do |namespace|
637
+ namespace.map(:show).to("/admin/:id/show")
638
+ namespace.map(:destroy).to("/admin/:id/destroy")
639
+ end
640
+
641
+ namespace :admin do
642
+ get :show do
643
+ "admin show for #{params[:id]}"
644
+ end
645
+
646
+ get :destroy do
647
+ "admin destroy for #{params[:id]}"
648
+ end
649
+ end
650
+
651
+ You can then reference the urls using the same <tt>url_for</tt> method:
652
+
653
+ <%= link_to 'admin page', url_for(:admin, :show, :id => 25) %>
654
+ <%= link_to 'admin page', url_for(:admin, :update, :id => 25) %>
655
+ <%= link_to 'admin page', url_for(:admin, :show, :id => 25) %>
656
+
657
+ You can freely use both named route aliases and traditional Sinatra routes in the same application without conflict.
658
+
659
+ See the wiki article for additional information: <http://wiki.github.com/nesquena/sinatra_more/routingplugin>
580
660
 
581
661
  == Sinatra Generators
582
662
 
@@ -642,6 +722,7 @@ See the wiki article for additional information: <http://wiki.github.com/nesquen
642
722
  * Thanks to sbfaulkner for the <tt>sinatra-helpers</tt> code that I browsed through many times while starting this library.
643
723
  * Thanks to vestel for the excellent modified <tt>pony</tt> fork which in part powers the mailer_plugin (http://github.com/vestel/pony)
644
724
  * Thanks to focat and sinatra-content-for library (http://github.com/focat/sinatra-content-for) for a good content_for starting point
725
+ * Thanks to bcarlso for the snap sinatra library (http://github.com/bcarlso/snap) which was a starting point for named routes
645
726
  * Thanks to wycats and others for the awesome Thor gem which made creating the sinatra generator relatively painless
646
727
  * Thanks to wycats and others for the bundler gem which made bundling the required gems for an application easy
647
728
 
data/ROADMAP CHANGED
@@ -18,37 +18,9 @@ Framework structure:
18
18
  * sinatra-mailer (mail handling for sinatra applications) <= from MailerPlugin
19
19
  * sinatra-gen (easy generation of and for sinatra apps) <= from Generator
20
20
  * sinatra-admin (admin management dashboard for content) <= from Lipsiadmin (ported)
21
+ * sinatra-routing (sinatra route mapping system) <= from RoutingPlugin
21
22
  * sinatra-cache (page and fragment caching support)
22
- * sinatra-mapping (sinatra route mapping system)
23
23
 
24
- 'sinatra-mapping' Routing Concept:
25
-
26
- namespace :admin do
27
- get :show do
28
- ...
29
- end
30
- end
31
-
32
- get :accounts do
33
- ...
34
- end
35
-
36
- map(:admin, :show).to("/my-admin/:id/show")
37
- map(:accounts).to("/show-me-my/accounts")
38
-
39
- # or
40
-
41
- map :admin do |namespace|
42
- namespace.map(:show).to("/my-admin/:id/show")
43
- end
44
-
45
- # and to use
46
- link_to "Show Admin", admin_show_path(:id => 5)
47
- link_to "Accounts", accounts_path
48
- # or maybe
49
- link_to "Show Admin", url_for(:admin, :show, :id => 5)
50
- link_to "Accounts", url_for(:accounts)
51
-
52
24
  'sinatra-cache' Caching concept:
53
25
 
54
26
  # in models
@@ -75,4 +47,29 @@ Framework structure:
75
47
  # page
76
48
  get '/example', :cache => true do
77
49
  haml_template 'accounts/index'
78
- end
50
+ end
51
+
52
+ 'sinatra-routing' Routing Concept:
53
+
54
+ namespace :admin do
55
+ get :show do
56
+ ...
57
+ end
58
+ end
59
+
60
+ get :accounts do
61
+ ...
62
+ end
63
+
64
+ map(:admin, :show).to("/my-admin/:id/show")
65
+ map(:accounts).to("/show-me-my/accounts")
66
+
67
+ # or
68
+
69
+ map :admin do |namespace|
70
+ namespace.map(:show).to("/my-admin/:id/show")
71
+ end
72
+
73
+ # and to use
74
+ link_to "Show Admin", url_for(:admin, :show, :id => 5)
75
+ link_to "Accounts", url_for(:accounts)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.13
1
+ 0.3.14
@@ -6,6 +6,12 @@ class <%= @class_name %> < Sinatra::Application
6
6
  # Required middleware
7
7
  use Rack::Session::Cookie
8
8
  use Rack::Flash
9
+
10
+ # Includes all necessary sinatra_more helpers
11
+ register SinatraMore::MarkupPlugin
12
+ register SinatraMore::RenderPlugin
13
+ register SinatraMore::MailerPlugin
14
+ register SinatraMore::RoutingPlugin
9
15
 
10
16
  # Requires the initializer modules which configure specific components
11
17
  Dir[File.dirname(__FILE__) + '/initializers/*.rb'].each do |file|
@@ -23,10 +29,7 @@ class <%= @class_name %> < Sinatra::Application
23
29
  # Require all the folders and files necessary to run the application
24
30
  file_loading_paths.each { |load_path| Dir[root_path(load_path)].each { |file| require file } }
25
31
 
26
- # Includes all necessary sinatra_more helpers
27
- register SinatraMore::MarkupPlugin
28
- register SinatraMore::RenderPlugin
29
- register SinatraMore::MailerPlugin
32
+ # Require Warden plugin below to allow User to be loaded
30
33
  register SinatraMore::WardenPlugin
31
34
 
32
35
  # Required helpers
@@ -0,0 +1,25 @@
1
+ module SinatraMore
2
+ class NamedRoute
3
+ # Constructs the NamedRoute which accepts the application and
4
+ # the route alias names to register (i.e [:account] or [:admin, :show])
5
+ # NamedRoute.new(@app, :admin, :show)
6
+ def initialize(app, *names)
7
+ @app = app
8
+ @names = names.flatten
9
+ end
10
+
11
+ # Used to define the url mapping to the supplied alias
12
+ # NamedRoute.new(@app, :account).to('/account/path')
13
+ def to(path)
14
+ @app.named_paths[@names] = path
15
+ end
16
+
17
+ # Used to define the url mappings for child aliases within a namespace
18
+ # Invokes map on the application itself, appending the namespace to the route
19
+ # NamedRoute.new(@app, :admin).map(:show).to('/admin/show')
20
+ # is equivalent to NamedRoute.new(@app, :admin, :show).to('/admin/show')
21
+ def map(*args, &block)
22
+ @app.map(*args.unshift(@names), &block)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module SinatraMore
2
+ module RoutingHelpers
3
+ # Used to retrieve the full url for a given named route alias from the named_paths data
4
+ # Accepts parameters which will be substituted into the url if necessary
5
+ # url_for(:accounts) => '/accounts'
6
+ # url_for(:account, :id => 5) => '/account/5'
7
+ # url_for(:admin, show, :id => 5, :name => "demo") => '/admin/path/5/demo'
8
+ def url_for(*names)
9
+ values = names.extract_options!
10
+ mapped_url = self.class.named_paths[names]
11
+ result_url = String.new(mapped_url)
12
+ result_url.scan(%r{/?(:\S+?)(?:/|$)}).each do |placeholder|
13
+ value_key = placeholder[0][1..-1].to_sym
14
+ result_url.gsub!(Regexp.new(placeholder[0]), values[value_key].to_s)
15
+ end
16
+ result_url
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/support_lite'
2
+ Dir[File.dirname(__FILE__) + '/routing_plugin/**/*.rb'].each {|file| load file }
3
+
4
+ module SinatraMore
5
+ module RoutingPlugin
6
+ def self.registered(app)
7
+ # Named paths stores the named route aliases mapping to the url
8
+ # i.e { [:account] => '/account/path', [:admin, :show] => '/admin/show/:id' }
9
+ app.set :named_paths, {}
10
+ app.helpers SinatraMore::RoutingHelpers
11
+
12
+ # map constructs a mapping between a named route and a specified alias
13
+ # the mapping url can contain url query parameters
14
+ # map(:accounts).to('/accounts/url')
15
+ # map(:admin, :show).to('/admin/show/:id')
16
+ # map(:admin) { |namespace| namespace.map(:show).to('/admin/show/:id') }
17
+ def map(*args, &block)
18
+ named_router = SinatraMore::NamedRoute.new(self, *args)
19
+ block_given? ? block.call(named_router) : named_router
20
+ end
21
+
22
+ # Used to define namespaced route configurations in order to group similar routes
23
+ # Class evals the routes but with the namespace assigned which will append to each route
24
+ # namespace(:admin) { get(:show) { "..." } }
25
+ def namespace(name, &block)
26
+ original, @_namespace = @_namespace, name
27
+ self.class_eval(&block)
28
+ @_namespace = original
29
+ end
30
+
31
+ # Hijacking route method in sinatra to replace a route alias (i.e :account) with the full url string mapping
32
+ # Supports namespaces by accessing the instance variable and appending this to the route alias name
33
+ # If the path is not a symbol, nothing is changed and the original route method is invoked
34
+ def route(verb, path, options={}, &block)
35
+ route_name = [@_namespace, path].flatten.compact
36
+ path = named_paths[route_name] if path.kind_of? Symbol
37
+ super verb, path, options, &block
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/sinatra_more.rb CHANGED
@@ -3,4 +3,5 @@ require 'sinatra/base'
3
3
  require File.join(File.dirname(__FILE__) + '/sinatra_more/markup_plugin')
4
4
  require File.join(File.dirname(__FILE__) + '/sinatra_more/render_plugin')
5
5
  require File.join(File.dirname(__FILE__) + '/sinatra_more/warden_plugin')
6
- require File.join(File.dirname(__FILE__) + '/sinatra_more/mailer_plugin')
6
+ require File.join(File.dirname(__FILE__) + '/sinatra_more/mailer_plugin')
7
+ require File.join(File.dirname(__FILE__) + '/sinatra_more/routing_plugin')
data/sinatra_more.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{sinatra_more}
8
- s.version = "0.3.13"
8
+ s.version = "0.3.14"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nathan Esquenazi"]
12
- s.date = %q{2009-11-12}
12
+ s.date = %q{2009-11-13}
13
13
  s.default_executable = %q{sinatra_gen}
14
14
  s.description = %q{Expands sinatra with standard helpers and tools to allow for complex applications}
15
15
  s.email = %q{nesquena@gmail.com}
@@ -84,6 +84,9 @@ Gem::Specification.new do |s|
84
84
  "lib/sinatra_more/markup_plugin/tag_helpers.rb",
85
85
  "lib/sinatra_more/render_plugin.rb",
86
86
  "lib/sinatra_more/render_plugin/render_helpers.rb",
87
+ "lib/sinatra_more/routing_plugin.rb",
88
+ "lib/sinatra_more/routing_plugin/named_route.rb",
89
+ "lib/sinatra_more/routing_plugin/routing_helpers.rb",
87
90
  "lib/sinatra_more/support_lite.rb",
88
91
  "lib/sinatra_more/warden_plugin.rb",
89
92
  "lib/sinatra_more/warden_plugin/warden_helpers.rb",
@@ -116,6 +119,8 @@ Gem::Specification.new do |s|
116
119
  "test/fixtures/render_app/views/template/_user.haml",
117
120
  "test/fixtures/render_app/views/template/haml_template.haml",
118
121
  "test/fixtures/render_app/views/template/some_template.haml",
122
+ "test/fixtures/routing_app/app.rb",
123
+ "test/fixtures/routing_app/views/index.haml",
119
124
  "test/fixtures/warden_app/app.rb",
120
125
  "test/fixtures/warden_app/views/dashboard.haml",
121
126
  "test/generators/test_skeleton_generator.rb",
@@ -130,6 +135,7 @@ Gem::Specification.new do |s|
130
135
  "test/markup_plugin/test_tag_helpers.rb",
131
136
  "test/test_mailer_plugin.rb",
132
137
  "test/test_render_plugin.rb",
138
+ "test/test_routing_plugin.rb",
133
139
  "test/test_warden_plugin.rb",
134
140
  "vendor/pony/lib/pony.rb",
135
141
  "vendor/pony/spec/base.rb",
@@ -145,6 +151,7 @@ Gem::Specification.new do |s|
145
151
  "test/fixtures/mailer_app/app.rb",
146
152
  "test/fixtures/markup_app/app.rb",
147
153
  "test/fixtures/render_app/app.rb",
154
+ "test/fixtures/routing_app/app.rb",
148
155
  "test/fixtures/warden_app/app.rb",
149
156
  "test/generators/test_skeleton_generator.rb",
150
157
  "test/helper.rb",
@@ -158,6 +165,7 @@ Gem::Specification.new do |s|
158
165
  "test/markup_plugin/test_tag_helpers.rb",
159
166
  "test/test_mailer_plugin.rb",
160
167
  "test/test_render_plugin.rb",
168
+ "test/test_routing_plugin.rb",
161
169
  "test/test_warden_plugin.rb"
162
170
  ]
163
171
 
@@ -0,0 +1,44 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra_more'
3
+ require 'haml'
4
+
5
+ class RoutingDemo < Sinatra::Base
6
+ register SinatraMore::RoutingPlugin
7
+
8
+ configure do
9
+ set :root, File.dirname(__FILE__)
10
+ end
11
+
12
+ map(:admin, :show).to("/admin/:id/show")
13
+ map :admin do |namespace|
14
+ namespace.map(:update).to("/admin/:id/update/:name")
15
+ namespace.map(:destroy).to("/admin/:id/destroy")
16
+ end
17
+ map(:account).to("/the/accounts/:name/path/:id/end")
18
+ map(:accounts).to("/the/accounts/index/?")
19
+
20
+ namespace :admin do
21
+ get :show do
22
+ "<p>admin show for id #{params[:id]}</p>"
23
+ end
24
+
25
+ get :update do
26
+ "<p>updated admin with id #{params[:id]} and name #{params[:name]}</p>"
27
+ end
28
+
29
+ get :destroy do
30
+ "<p>destroy admin with id #{params[:id]}</p>"
31
+ end
32
+ end
33
+ get :account do
34
+ "<h1>the account url for #{params[:name]} and id #{params[:id]}</h1>"
35
+ end
36
+
37
+ get :accounts do
38
+ "<h1>the accounts index</h1>"
39
+ end
40
+
41
+ get '/links' do
42
+ haml :index
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ %p.admin_url= url_for(:admin, :show, :id => 25)
2
+ %p.admin_url2= url_for(:admin, :update, :id => 10, :name => "test")
3
+ %p.admin_url3= url_for(:admin, :destroy, :id => 12)
4
+ %p.account_url= url_for(:account, :name => 'foobar', :id => 10)
5
+ %p.accounts_index=url_for(:accounts)
@@ -0,0 +1,56 @@
1
+ require 'helper'
2
+ require 'fixtures/routing_app/app'
3
+
4
+ class TestRoutingPlugin < Test::Unit::TestCase
5
+ def app
6
+ RoutingDemo.tap { |app| app.set :environment, :test }
7
+ end
8
+
9
+ context 'for links list displaying routes' do
10
+ setup { visit '/links' }
11
+ should 'display account route links' do
12
+ assert_have_selector :p, :class => 'account_url', :content => '/the/accounts/foobar/path/10/end'
13
+ assert_have_selector :p, :class => 'accounts_index', :content => '/the/accounts/index'
14
+ end
15
+ should "display admin route links" do
16
+ assert_have_selector :p, :class => 'admin_url', :content => '/admin/25/show'
17
+ assert_have_selector :p, :class => 'admin_url2', :content => '/admin/10/update/test'
18
+ assert_have_selector :p, :class => 'admin_url3', :content => '/admin/12/destroy'
19
+ end
20
+ end
21
+
22
+ context 'for no namespaced account route' do
23
+ setup { visit '/the/accounts/demo/path/5/end'}
24
+ should "return proper account text" do
25
+ assert_have_selector :h1, :content => "the account url for demo and id 5"
26
+ end
27
+ end
28
+
29
+ context 'for no namespaced accounts index route' do
30
+ setup { visit '/the/accounts/index/'}
31
+ should "return proper account text" do
32
+ assert_have_selector :h1, :content => "the accounts index"
33
+ end
34
+ end
35
+
36
+ context 'for admin show url' do
37
+ setup { visit '/admin/50/show' }
38
+ should "return proper admin test" do
39
+ assert_have_selector :p, :content => "admin show for id 50"
40
+ end
41
+ end
42
+
43
+ context 'for admin update url' do
44
+ setup { visit '/admin/15/update/demo' }
45
+ should "return proper update text" do
46
+ assert_have_selector :p, :content => "updated admin with id 15 and name demo"
47
+ end
48
+ end
49
+
50
+ context 'for admin destroy url' do
51
+ setup { visit '/admin/60/destroy' }
52
+ should "return proper destroy text" do
53
+ assert_have_selector :p, :content => "destroy admin with id 60"
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra_more
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13
4
+ version: 0.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Esquenazi
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-12 00:00:00 -08:00
12
+ date: 2009-11-13 00:00:00 -08:00
13
13
  default_executable: sinatra_gen
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -187,6 +187,9 @@ files:
187
187
  - lib/sinatra_more/markup_plugin/tag_helpers.rb
188
188
  - lib/sinatra_more/render_plugin.rb
189
189
  - lib/sinatra_more/render_plugin/render_helpers.rb
190
+ - lib/sinatra_more/routing_plugin.rb
191
+ - lib/sinatra_more/routing_plugin/named_route.rb
192
+ - lib/sinatra_more/routing_plugin/routing_helpers.rb
190
193
  - lib/sinatra_more/support_lite.rb
191
194
  - lib/sinatra_more/warden_plugin.rb
192
195
  - lib/sinatra_more/warden_plugin/warden_helpers.rb
@@ -219,6 +222,8 @@ files:
219
222
  - test/fixtures/render_app/views/template/_user.haml
220
223
  - test/fixtures/render_app/views/template/haml_template.haml
221
224
  - test/fixtures/render_app/views/template/some_template.haml
225
+ - test/fixtures/routing_app/app.rb
226
+ - test/fixtures/routing_app/views/index.haml
222
227
  - test/fixtures/warden_app/app.rb
223
228
  - test/fixtures/warden_app/views/dashboard.haml
224
229
  - test/generators/test_skeleton_generator.rb
@@ -233,6 +238,7 @@ files:
233
238
  - test/markup_plugin/test_tag_helpers.rb
234
239
  - test/test_mailer_plugin.rb
235
240
  - test/test_render_plugin.rb
241
+ - test/test_routing_plugin.rb
236
242
  - test/test_warden_plugin.rb
237
243
  - vendor/pony/lib/pony.rb
238
244
  - vendor/pony/spec/base.rb
@@ -270,6 +276,7 @@ test_files:
270
276
  - test/fixtures/mailer_app/app.rb
271
277
  - test/fixtures/markup_app/app.rb
272
278
  - test/fixtures/render_app/app.rb
279
+ - test/fixtures/routing_app/app.rb
273
280
  - test/fixtures/warden_app/app.rb
274
281
  - test/generators/test_skeleton_generator.rb
275
282
  - test/helper.rb
@@ -283,4 +290,5 @@ test_files:
283
290
  - test/markup_plugin/test_tag_helpers.rb
284
291
  - test/test_mailer_plugin.rb
285
292
  - test/test_render_plugin.rb
293
+ - test/test_routing_plugin.rb
286
294
  - test/test_warden_plugin.rb