wsdsl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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