wsdsl 0.0.1

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.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 Matt Aimonetti
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ --
23
+
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Web Service DSL
2
+
3
+ WSDSL is a simple DSL allowind developers to simply describe and
4
+ document their web APIS. For instance:
5
+
6
+
7
+ describe_service "hello_world" do |service|
8
+ service.formats :xml
9
+ service.http_verb :get
10
+ service.disable_auth # on by default
11
+
12
+ service.param.string :name, :default => 'World'
13
+
14
+ service.response do |response|
15
+ response.element(:name => "greeting") do |e|
16
+ e.attribute "message" => :string, :doc => "The greeting message sent back."
17
+ end
18
+ end
19
+
20
+ service.documentation do |doc|
21
+ doc.overall "This service provides a simple hello world implementation example."
22
+ doc.params :name, "The name of the person to greet."
23
+ doc.example "<code>http://example.com/hello_world.xml?name=Matt</code>"
24
+ end
25
+
26
+ end
27
+
28
+
29
+ Or a more complex example:
30
+
31
+ SpecOptions = ['RSpec', 'Bacon'] # usually pulled from a model
32
+
33
+ describe_service "wsdsl/test.xml" do |service|
34
+ service.formats :xml, :json
35
+ service.http_verb :get
36
+
37
+ service.params do |p|
38
+ p.string :framework, :in => SpecOptions, :null => false, :required => true
39
+
40
+ p.datetime :timestamp, :default => Time.now
41
+ p.string :alpha, :in => ['a', 'b', 'c']
42
+ p.string :version, :null => false
43
+ p.integer :num, :minvalue => 42
44
+ end
45
+
46
+ # service.param :delta, :optional => true, :type => 'float'
47
+ # if the optional flag isn't passed, the param is considered required.
48
+ # service.param :epsilon, :type => 'string'
49
+
50
+ service.params.namespace :user do |user|
51
+ user.integer :id, :required => :true
52
+ end
53
+
54
+ # the response contains a list of player creation ratings each object in the list
55
+
56
+ service.response do |response|
57
+ response.element(:name => "player_creation_ratings") do |e|
58
+ e.attribute :id => :integer, :doc => "id doc"
59
+ e.attribute :is_accepted => :boolean, :doc => "is accepted doc"
60
+ e.attribute :name => :string, :doc => "name doc"
61
+
62
+ e.array :name => 'player_creation_rating', :type => 'PlayerCreationRating' do |a|
63
+ a.attribute :comments => :string, :doc => "comments doc"
64
+ a.attribute :player_id => :integer, :doc => "player_id doc"
65
+ a.attribute :rating => :integer, :doc => "rating doc"
66
+ a.attribute :username => :string, :doc => "username doc"
67
+ end
68
+ end
69
+ end
70
+
71
+ service.documentation do |doc|
72
+ # doc.overall <markdown description text>
73
+ doc.overall <<-DOC
74
+ This is a test service used to test the framework.
75
+ DOC
76
+
77
+ # doc.params <name>, <definition>
78
+ doc.params :framework, "The test framework used, could be one of the two following: #{SpecOptions.join(", ")}."
79
+ doc.params :version, "The version of the framework to use."
80
+
81
+ # doc.example <markdown text>
82
+ doc.example <<-DOC
83
+ The most common way to use this service looks like that:
84
+ http://example.com/wsdsl/test.xml?framework=rspec&version=2.0.0
85
+ DOC
86
+ end
87
+ end
88
+
89
+
90
+ ## Test suite
91
+
92
+ This library comes with a test suite requiring Ruby 1.9.2
93
+ The following gems need to be available:
94
+ Rspec, Rack, Sinatra
95
+
96
+
97
+ ## Copyright
98
+
99
+ Copyright (c) 2011 Matt Aimonetti. See LICENSE for
100
+ further details.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require'rake'
3
+
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
7
+ gem.name = "wsdsl"
8
+ gem.homepage = "http://github.com/mattetti/wsdsl"
9
+ gem.license = "MIT"
10
+ gem.summary = %Q{Web Service DSL}
11
+ gem.description = %Q{A Ruby DSL describing Web Services without implementation details.}
12
+ gem.email = "mattaimonetti@gmail.com"
13
+ gem.authors = ["Matt Aimonetti"]
14
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
15
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
16
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
17
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
18
+ end
19
+ Jeweler::RubygemsDotOrgTasks.new
20
+
21
+ require 'rspec/core'
22
+ require 'rspec/core/rake_task'
23
+ RSpec::Core::RakeTask.new(:spec) do |spec|
24
+ spec.pattern = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
28
+ spec.pattern = 'spec/**/*_spec.rb'
29
+ spec.rcov = true
30
+ end
31
+
32
+ task :default => :spec
33
+
34
+ require 'yard'
35
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,151 @@
1
+ class WSDSL
2
+ # Service documentation class
3
+ #
4
+ # @api public
5
+ class Documentation
6
+
7
+ # @api public
8
+ attr_reader :desc
9
+
10
+ # @api public
11
+ attr_reader :params_doc
12
+
13
+ # @api public
14
+ attr_reader :namespaced_params
15
+
16
+ # @api public
17
+ attr_reader :examples
18
+
19
+ # @api public
20
+ attr_reader :elements
21
+
22
+ # This class contains the documentation information regarding an element.
23
+ # Currently, elements are only used in the response info.
24
+ #
25
+ # @api public
26
+ class ElementDoc
27
+
28
+ # @api public
29
+ attr_reader :name, :attributes
30
+
31
+ # @param [String] The element's name
32
+ # @api public
33
+ def initialize(name)
34
+ # raise ArgumentError, "An Element doc needs to be initialize by passing a hash with a ':name' keyed entry." unless opts.is_a?(Hash) && opts.has_key?(:name)
35
+ @name = name
36
+ @attributes = {}
37
+ end
38
+
39
+ # @param [String] name The name of the attribute described
40
+ # @param [String] desc The description of the attribute
41
+ # @api public
42
+ def attribute(name, desc)
43
+ @attributes[name] = desc
44
+ end
45
+
46
+ end # of ElementDoc
47
+
48
+ # Namespaced param documentation
49
+ #
50
+ # @api public
51
+ class NamespacedParam
52
+
53
+ # @return [String, Symbol] The name of the namespaced, usually a symbol
54
+ # @api public
55
+ attr_reader :name
56
+
57
+ # @return [Hash] The list of params within the namespace
58
+ # @api public
59
+ attr_reader :params
60
+
61
+ # @api public
62
+ def initialize(name)
63
+ @name = name
64
+ @params = {}
65
+ end
66
+
67
+ # Sets the description/documentation of a specific namespaced param
68
+ #
69
+ # @return [String]
70
+ # @api public
71
+ def param(name, desc)
72
+ @params[name] = desc
73
+ end
74
+
75
+ end
76
+
77
+ # Initialize a Documentation object wrapping all the documentation aspect of the service.
78
+ # The response documentation is a Documentation instance living inside the service documentation object.
79
+ #
80
+ # @api public
81
+ def initialize
82
+ @params_doc = {}
83
+ @examples = []
84
+ @elements = []
85
+ @namespaced_params = []
86
+ end
87
+
88
+ # Sets or returns the overall description
89
+ #
90
+ # @param [String] desc Service overall description
91
+ # @api public
92
+ # @return [String] The overall service description
93
+ def overall(desc)
94
+ if desc.nil?
95
+ @desc
96
+ else
97
+ @desc = desc
98
+ end
99
+ end
100
+
101
+ # Sets the description/documentation of a specific param
102
+ #
103
+ # @return [String]
104
+ # @api public
105
+ def params(name, desc)
106
+ @params_doc[name] = desc
107
+ end
108
+ alias_method :param, :params
109
+
110
+ # Define a new namespaced param and yield it to the passed block
111
+ # if available.
112
+ #
113
+ # @return [Array] the namespaced params
114
+ # @api public
115
+ def namespace(ns_name)
116
+ new_ns_param = NamespacedParam.new(ns_name)
117
+ if block_given?
118
+ yield(new_ns_param)
119
+ end
120
+ @namespaced_params << new_ns_param
121
+ end
122
+
123
+ def response
124
+ @response ||= Documentation.new
125
+ end
126
+
127
+ # Service usage example
128
+ #
129
+ # @param [String] desc Usage example.
130
+ # @return [Array<String>] All the examples.
131
+ # @api public
132
+ def example(desc)
133
+ @examples << desc
134
+ end
135
+
136
+ # Add a new element to the doc
137
+ # currently only used for response doc
138
+ #
139
+ # @param [Hash] opts element's documentation options
140
+ # @yield [ElementDoc] The new element doc.
141
+ # @return [Array<ElementDoc>]
142
+ # @api public
143
+ def element(opts={})
144
+ element = ElementDoc.new(opts)
145
+ yield(element)
146
+ @elements << element
147
+ end
148
+
149
+
150
+ end # of Documentation
151
+ end
@@ -0,0 +1,30 @@
1
+ # Module used to extend {WSDSL} and add {#load_sinatra_route} to services.
2
+ # This code is Sinatra specific and therefore lives outside the {WSDSL}
3
+ # @see {WSDSL}
4
+ # @api public
5
+ module WSDSLSinatraExtension
6
+
7
+ # Defines a sinatra service route based on its settings
8
+ #
9
+ # @return [Nil]
10
+ # @api private
11
+ def load_sinatra_route
12
+ service = self
13
+ upcase_verb = service.verb.to_s.upcase
14
+ puts "/#{self.url} -> #{self.controller_name}##{self.action} - (#{upcase_verb})"
15
+
16
+ # Define the route directly to save some object allocations on the critical path
17
+ # Note that we are using a private API to define the route and that unlike sinatra usual DSL
18
+ # we do NOT define a HEAD route for every GET route.
19
+ Sinatra::Base.send(:route, upcase_verb, "/#{self.url}") do
20
+ service.controller_dispatch(self)
21
+ end
22
+
23
+ # Other alternative to the route definition, this time using the public API
24
+ # self.send(verb, "/#{service.url}") do
25
+ # service.controller_dispatch(self)
26
+ # end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,80 @@
1
+ require 'forwardable'
2
+ require File.expand_path('./../params_verification', File.dirname(__FILE__))
3
+
4
+ # Base code shared by all service controllers
5
+ # This allows us to share code between controllers
6
+ # more precisely to render templates and in general to use sinatra helpers
7
+ #
8
+ # @see Sinatra::Base and Sinatra::Helpers
9
+ # @api public
10
+ # @author Matt Aimonetti
11
+ class SinatraServiceController
12
+ extend Forwardable
13
+
14
+ # The service controller might be loaded outside of a Sinatra App
15
+ # in this case, we don't need to load the helpers
16
+ if Object.const_defined?(:Sinatra)
17
+ include Sinatra::Helpers
18
+ end
19
+
20
+ class AuthenticationFailed < StandardError; end
21
+
22
+ # @return [WSDSL] The service served by this controller
23
+ # @api public
24
+ attr_reader :service
25
+
26
+ # @return [Sinatra::Application]
27
+ # @api public
28
+ attr_reader :app
29
+
30
+ # @return [Hash]
31
+ # @api public
32
+ attr_reader :env
33
+
34
+ # @return [Sinatra::Request]
35
+ # @see http://rubydoc.info/github/sinatra/sinatra/Sinatra/Request
36
+ # @api public
37
+ attr_reader :request
38
+
39
+ # @return [Sinatra::Response]
40
+ # @see http://rubydoc.info/github/sinatra/sinatra/Sinatra/Response
41
+ # @api public
42
+ attr_reader :response
43
+
44
+ # @return [Hash]
45
+ # @api public
46
+ attr_accessor :params
47
+
48
+ # @param [Sinatra::Application] app The Sinatra app used as a reference and to access request params
49
+ # @param [WSDSL] service The service served by this controller
50
+ # @raise [ParamError, NoParamsDefined, MissingParam, UnexpectedParam, InvalidParamType, InvalidParamValue]
51
+ # If the params don't validate one of the {ParamsVerification} errors will be raised.
52
+ # @api public
53
+ def initialize(app, service)
54
+ @app = app
55
+ @env = app.env
56
+ @request = app.request
57
+ @response = app.response
58
+ @service = service
59
+
60
+ # raises an exception if the params are not valid
61
+ # otherwise update the app params with potentially new params (using default values)
62
+ # note that if a type if mentioned for a params, the object will be cast to this object type
63
+ @params = app.params = ParamsVerification.validate!(app.params, service.defined_params)
64
+
65
+ # Authentication check
66
+ if service.auth_required
67
+ raise AuthenticationFailed unless logged_in?
68
+ end
69
+ end
70
+
71
+
72
+ # Forwarding some methods to the underlying app object
73
+ def_delegators :app, :settings, :halt, :compile_template, :session
74
+
75
+ # Returns true or false if the player is logged in.
76
+ def logged_in?
77
+ !session[:player_id].nil?
78
+ end
79
+
80
+ end
data/lib/inflection.rb ADDED
@@ -0,0 +1,479 @@
1
+ #Copyright (c) 2010 Dan Kubb
2
+
3
+ #Permission is hereby granted, free of charge, to any person obtaining
4
+ #a copy of this software and associated documentation files (the
5
+ #"Software"), to deal in the Software without restriction, including
6
+ #without limitation the rights to use, copy, modify, merge, publish,
7
+ #distribute, sublicense, and/or sell copies of the Software, and to
8
+ #permit persons to whom the Software is furnished to do so, subject to
9
+ #the following conditions:
10
+
11
+ #The above copyright notice and this permission notice shall be
12
+ #included in all copies or substantial portions of the Software.
13
+
14
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ #MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ #NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ #LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ #OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ #WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ #---
23
+
24
+
25
+ module Extlib
26
+
27
+ # = English Nouns Number Inflection.
28
+ #
29
+ # This module provides english singular <-> plural noun inflections.
30
+ module Inflection
31
+
32
+ class << self
33
+ # Take an underscored name and make it into a camelized name
34
+ #
35
+ # @example
36
+ # "egg_and_hams".classify #=> "EggAndHam"
37
+ # "enlarged_testes".classify #=> "EnlargedTestis"
38
+ # "post".classify #=> "Post"
39
+ #
40
+ def classify(name)
41
+ words = name.to_s.sub(/.*\./, '').split('_')
42
+ words[-1] = singularize(words[-1])
43
+ words.collect { |word| word.capitalize }.join
44
+ end
45
+
46
+ # By default, camelize converts strings to UpperCamelCase.
47
+ #
48
+ # camelize will also convert '/' to '::' which is useful for converting paths to namespaces
49
+ #
50
+ # @example
51
+ # "active_record".camelize #=> "ActiveRecord"
52
+ # "active_record/errors".camelize #=> "ActiveRecord::Errors"
53
+ #
54
+ def camelize(lower_case_and_underscored_word, *args)
55
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
56
+ end
57
+
58
+
59
+ # The reverse of +camelize+. Makes an underscored form from the expression in the string.
60
+ #
61
+ # Changes '::' to '/' to convert namespaces to paths.
62
+ #
63
+ # @example
64
+ # "ActiveRecord".underscore #=> "active_record"
65
+ # "ActiveRecord::Errors".underscore #=> active_record/errors
66
+ #
67
+ def underscore(camel_cased_word)
68
+ camel_cased_word.to_const_path
69
+ end
70
+
71
+ # Capitalizes the first word and turns underscores into spaces and strips _id.
72
+ # Like titleize, this is meant for creating pretty output.
73
+ #
74
+ # @example
75
+ # "employee_salary" #=> "Employee salary"
76
+ # "author_id" #=> "Author"
77
+ def humanize(lower_case_and_underscored_word)
78
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, '').tr('_', ' ').capitalize
79
+ end
80
+
81
+ # Removes the module part from the expression in the string
82
+ #
83
+ # @example
84
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections"
85
+ # "Inflections".demodulize #=> "Inflections"
86
+ def demodulize(class_name_in_module)
87
+ class_name_in_module.to_s.gsub(/^.*::/, '')
88
+ end
89
+
90
+ # Create the name of a table like Rails does for models to table names. This method
91
+ # uses the pluralize method on the last word in the string.
92
+ #
93
+ # @example
94
+ # "RawScaledScorer".tableize #=> "raw_scaled_scorers"
95
+ # "EnlargedTestis".tableize #=> "enlarged_testes"
96
+ # "egg_and_ham".tableize #=> "egg_and_hams"
97
+ # "fancyCategory".tableize #=> "fancy_categories"
98
+ def tableize(class_name)
99
+ words = class_name.to_const_path.tr('/', '_').split('_')
100
+ words[-1] = pluralize(words[-1])
101
+ words.join('_')
102
+ end
103
+
104
+ # Creates a foreign key name from a class name.
105
+ #
106
+ # @example
107
+ # "Message".foreign_key #=> "message_id"
108
+ # "Admin::Post".foreign_key #=> "post_id"
109
+ def foreign_key(class_name, key = "id")
110
+ underscore(demodulize(class_name.to_s)) << "_" << key.to_s
111
+ end
112
+
113
+ # Constantize tries to find a declared constant with the name specified
114
+ # in the string. It raises a NameError when the name is not in CamelCase
115
+ # or is not initialized.
116
+ #
117
+ # @example
118
+ # "Module".constantize #=> Module
119
+ # "Class".constantize #=> Class
120
+ def constantize(camel_cased_word)
121
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
122
+ raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
123
+ end
124
+
125
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
126
+ end
127
+ end
128
+
129
+ @singular_of = {}
130
+ @plural_of = {}
131
+
132
+ @singular_rules = []
133
+ @plural_rules = []
134
+
135
+ class << self
136
+ # Defines a general inflection exception case.
137
+ #
138
+ # ==== Parameters
139
+ # singular<String>::
140
+ # singular form of the word
141
+ # plural<String>::
142
+ # plural form of the word
143
+ #
144
+ # ==== Examples
145
+ #
146
+ # Here we define erratum/errata exception case:
147
+ #
148
+ # English::Inflect.word "erratum", "errata"
149
+ #
150
+ # In case singular and plural forms are the same omit
151
+ # second argument on call:
152
+ #
153
+ # English::Inflect.word 'information'
154
+ def word(singular, plural=nil)
155
+ plural = singular unless plural
156
+ singular_word(singular, plural)
157
+ plural_word(singular, plural)
158
+ end
159
+
160
+ def clear(type = :all)
161
+ if type == :singular || type == :all
162
+ @singular_of = {}
163
+ @singular_rules = []
164
+ @singularization_rules, @singularization_regex = nil, nil
165
+ end
166
+ if type == :plural || type == :all
167
+ @singular_of = {}
168
+ @singular_rules = []
169
+ @singularization_rules, @singularization_regex = nil, nil
170
+ end
171
+ end
172
+
173
+
174
+ # Define a singularization exception.
175
+ #
176
+ # ==== Parameters
177
+ # singular<String>::
178
+ # singular form of the word
179
+ # plural<String>::
180
+ # plural form of the word
181
+ def singular_word(singular, plural)
182
+ @singular_of[plural] = singular
183
+ @singular_of[plural.capitalize] = singular.capitalize
184
+ end
185
+
186
+ # Define a pluralization exception.
187
+ #
188
+ # ==== Parameters
189
+ # singular<String>::
190
+ # singular form of the word
191
+ # plural<String>::
192
+ # plural form of the word
193
+ def plural_word(singular, plural)
194
+ @plural_of[singular] = plural
195
+ @plural_of[singular.capitalize] = plural.capitalize
196
+ end
197
+
198
+ # Define a general rule.
199
+ #
200
+ # ==== Parameters
201
+ # singular<String>::
202
+ # ending of the word in singular form
203
+ # plural<String>::
204
+ # ending of the word in plural form
205
+ # whole_word<Boolean>::
206
+ # for capitalization, since words can be
207
+ # capitalized (Man => Men) #
208
+ # ==== Examples
209
+ # Once the following rule is defined:
210
+ # English::Inflect.rule 'y', 'ies'
211
+ #
212
+ # You can see the following results:
213
+ # irb> "fly".plural
214
+ # => flies
215
+ # irb> "cry".plural
216
+ # => cries
217
+ # Define a general rule.
218
+
219
+ def rule(singular, plural, whole_word = false)
220
+ singular_rule(singular, plural)
221
+ plural_rule(singular, plural)
222
+ word(singular, plural) if whole_word
223
+ end
224
+
225
+ # Define a singularization rule.
226
+ #
227
+ # ==== Parameters
228
+ # singular<String>::
229
+ # ending of the word in singular form
230
+ # plural<String>::
231
+ # ending of the word in plural form
232
+ #
233
+ # ==== Examples
234
+ # Once the following rule is defined:
235
+ # English::Inflect.singular_rule 'o', 'oes'
236
+ #
237
+ # You can see the following results:
238
+ # irb> "heroes".singular
239
+ # => hero
240
+ def singular_rule(singular, plural)
241
+ @singular_rules << [singular, plural]
242
+ end
243
+
244
+ # Define a plurualization rule.
245
+ #
246
+ # ==== Parameters
247
+ # singular<String>::
248
+ # ending of the word in singular form
249
+ # plural<String>::
250
+ # ending of the word in plural form
251
+ #
252
+ # ==== Examples
253
+ # Once the following rule is defined:
254
+ # English::Inflect.singular_rule 'fe', 'ves'
255
+ #
256
+ # You can see the following results:
257
+ # irb> "wife".plural
258
+ # => wives
259
+ def plural_rule(singular, plural)
260
+ @plural_rules << [singular, plural]
261
+ end
262
+
263
+ # Read prepared singularization rules.
264
+ def singularization_rules
265
+ if defined?(@singularization_regex) && @singularization_regex
266
+ return [@singularization_regex, @singularization_hash]
267
+ end
268
+ # No sorting needed: Regexen match on longest string
269
+ @singularization_regex = Regexp.new("(" + @singular_rules.map {|s,p| p}.join("|") + ")$", "i")
270
+ @singularization_hash = Hash[*@singular_rules.flatten].invert
271
+ [@singularization_regex, @singularization_hash]
272
+ end
273
+
274
+ # Read prepared pluralization rules.
275
+ def pluralization_rules
276
+ if defined?(@pluralization_regex) && @pluralization_regex
277
+ return [@pluralization_regex, @pluralization_hash]
278
+ end
279
+ @pluralization_regex = Regexp.new("(" + @plural_rules.map {|s,p| s}.join("|") + ")$", "i")
280
+ @pluralization_hash = Hash[*@plural_rules.flatten]
281
+ [@pluralization_regex, @pluralization_hash]
282
+ end
283
+
284
+ attr_reader :singular_of, :plural_of
285
+
286
+ # Convert an English word from plural to singular.
287
+ #
288
+ # "boys".singular #=> boy
289
+ # "tomatoes".singular #=> tomato
290
+ #
291
+ # ==== Parameters
292
+ # word<String>:: word to singularize
293
+ #
294
+ # ==== Returns
295
+ # <String>:: singularized form of word
296
+ #
297
+ # ==== Notes
298
+ # Aliased as singularize (a Railism)
299
+ def singular(word)
300
+ if result = singular_of[word]
301
+ return result.dup
302
+ end
303
+ result = word.dup
304
+ regex, hash = singularization_rules
305
+ result.sub!(regex) {|m| hash[m]}
306
+ singular_of[word] = result
307
+ return result
308
+ end
309
+
310
+ # Alias for #singular (a Railism).
311
+ #
312
+ alias_method(:singularize, :singular)
313
+
314
+ # Convert an English word from singular to plural.
315
+ #
316
+ # "boy".plural #=> boys
317
+ # "tomato".plural #=> tomatoes
318
+ #
319
+ # ==== Parameters
320
+ # word<String>:: word to pluralize
321
+ #
322
+ # ==== Returns
323
+ # <String>:: pluralized form of word
324
+ #
325
+ # ==== Notes
326
+ # Aliased as pluralize (a Railism)
327
+ def plural(word)
328
+ # special exceptions
329
+ return "" if word == ""
330
+ if result = plural_of[word]
331
+ return result.dup
332
+ end
333
+ result = word.dup
334
+ regex, hash = pluralization_rules
335
+ result.sub!(regex) {|m| hash[m]}
336
+ plural_of[word] = result
337
+ return result
338
+ end
339
+
340
+ # Alias for #plural (a Railism).
341
+ alias_method(:pluralize, :plural)
342
+ end
343
+
344
+ # One argument means singular and plural are the same.
345
+
346
+ word 'equipment'
347
+ word 'fish'
348
+ word 'grass'
349
+ word 'hovercraft'
350
+ word 'information'
351
+ word 'milk'
352
+ word 'money'
353
+ word 'moose'
354
+ word 'plurals'
355
+ word 'postgres'
356
+ word 'rain'
357
+ word 'rice'
358
+ word 'series'
359
+ word 'sheep'
360
+ word 'species'
361
+ word 'status'
362
+
363
+ # Two arguments defines a singular and plural exception.
364
+ word 'alias' , 'aliases'
365
+ word 'analysis' , 'analyses'
366
+ word 'axis' , 'axes'
367
+ word 'basis' , 'bases'
368
+ word 'buffalo' , 'buffaloes'
369
+ word 'cactus' , 'cacti'
370
+ word 'crisis' , 'crises'
371
+ word 'criterion' , 'criteria'
372
+ word 'cross' , 'crosses'
373
+ word 'datum' , 'data'
374
+ word 'diagnosis' , 'diagnoses'
375
+ word 'drive' , 'drives'
376
+ word 'erratum' , 'errata'
377
+ word 'goose' , 'geese'
378
+ word 'index' , 'indices'
379
+ word 'life' , 'lives'
380
+ word 'louse' , 'lice'
381
+ word 'matrix' , 'matrices'
382
+ word 'medium' , 'media'
383
+ word 'mouse' , 'mice'
384
+ word 'movie' , 'movies'
385
+ word 'octopus' , 'octopi'
386
+ word 'ox' , 'oxen'
387
+ word 'phenomenon' , 'phenomena'
388
+ word 'plus' , 'plusses'
389
+ word 'potato' , 'potatoes'
390
+ word 'quiz' , 'quizzes'
391
+ word 'status' , 'status'
392
+ word 'status' , 'statuses'
393
+ word 'Swiss' , 'Swiss'
394
+ word 'testis' , 'testes'
395
+ word 'thesaurus' , 'thesauri'
396
+ word 'thesis' , 'theses'
397
+ word 'thief' , 'thieves'
398
+ word 'tomato' , 'tomatoes'
399
+ word 'torpedo' , 'torpedoes'
400
+ word 'vertex' , 'vertices'
401
+ word 'wife' , 'wives'
402
+
403
+ # One-way singularization exception (convert plural to singular).
404
+
405
+ # General rules.
406
+ rule 'person' , 'people', true
407
+ rule 'shoe' , 'shoes', true
408
+ rule 'hive' , 'hives', true
409
+ rule 'man' , 'men', true
410
+ rule 'child' , 'children', true
411
+ rule 'news' , 'news', true
412
+ rule 'rf' , 'rves'
413
+ rule 'af' , 'aves'
414
+ rule 'ero' , 'eroes'
415
+ rule 'man' , 'men'
416
+ rule 'ch' , 'ches'
417
+ rule 'sh' , 'shes'
418
+ rule 'ss' , 'sses'
419
+ rule 'ta' , 'tum'
420
+ rule 'ia' , 'ium'
421
+ rule 'ra' , 'rum'
422
+ rule 'ay' , 'ays'
423
+ rule 'ey' , 'eys'
424
+ rule 'oy' , 'oys'
425
+ rule 'uy' , 'uys'
426
+ rule 'y' , 'ies'
427
+ rule 'x' , 'xes'
428
+ rule 'lf' , 'lves'
429
+ rule 'ffe' , 'ffes'
430
+ rule 'afe' , 'aves'
431
+ rule 'ouse' , 'ouses'
432
+ # more cases of words ending in -oses not being singularized properly
433
+ # than cases of words ending in -osis
434
+ # rule 'osis' , 'oses'
435
+ rule 'ox' , 'oxes'
436
+ rule 'us' , 'uses'
437
+ rule '' , 's'
438
+
439
+ # One-way singular rules.
440
+
441
+ singular_rule 'of' , 'ofs' # proof
442
+ singular_rule 'o' , 'oes' # hero, heroes
443
+ singular_rule 'f' , 'ves'
444
+
445
+ # One-way plural rules.
446
+
447
+ #plural_rule 'fe' , 'ves' # safe, wife
448
+ plural_rule 's' , 'ses'
449
+ plural_rule 'ive' , 'ives' # don't want to snag wife
450
+ plural_rule 'fe' , 'ves' # don't want to snag perspectives
451
+
452
+ end
453
+ end
454
+
455
+ unless "".respond_to?(:singular)
456
+ class String
457
+ def singular
458
+ Extlib::Inflection.singular(self)
459
+ end
460
+ alias_method(:singularize, :singular)
461
+ end
462
+ end
463
+
464
+ unless "".respond_to?(:plural)
465
+ class String
466
+ def plural
467
+ Extlib::Inflection.plural(self)
468
+ end
469
+ alias_method(:pluralize, :plural)
470
+ end
471
+ end
472
+
473
+ unless "".respond_to?(:classify)
474
+ class String
475
+ def classify
476
+ Extlib::Inflection.classify(self)
477
+ end
478
+ end
479
+ end