poro 0.1.0

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.
@@ -0,0 +1,100 @@
1
+ module Poro
2
+ # When this module is mixed into a a persistent object, it adds the
3
+ # object oriented methods directly to the class, making it behave like a model
4
+ # in other ORMs.
5
+ #
6
+ # If this module is not mixed in, the object can still persist, but the context
7
+ # must be directly accessed to fetch, save, and remove the data.
8
+ #
9
+ # Additionally, this module also ensures that the appropriate
10
+ # primary key accessor methods exist on the object, creating them if necessary.
11
+ #
12
+ # WARNING: You should configure the primary keys on the class' Context before
13
+ # including this module if you want to use an accessor other than the
14
+ # Context's default.
15
+ #
16
+ # See Modelify::ClassMethods and Modelify::InstanceMethods for the methods
17
+ # added by this mixin.
18
+ #
19
+ # = TODO: Piecewise Modelfication
20
+ #
21
+ # Modelfication should be done in a piece-meal way, so that we can layer
22
+ # in features such as pk accesor generation, basic model methods, find methods,
23
+ # and hook methods. We may still keep this top-level include to add the full
24
+ # suite, but we should break up the pieces. (In doing this, we could to manage
25
+ # dependencies so that we get the basic pieces included in when we need things
26
+ # they depend on, but by not managing this, it gives more flexability for
27
+ # advanced users to replace segments.
28
+ module Modelify
29
+
30
+ def self.included(mod) # :nodoc:
31
+ mod.send(:extend, ClassMethods)
32
+ mod.send(:include, InstanceMethods)
33
+ mod.send(:include, FindMethods)
34
+
35
+ context = Context.fetch(mod)
36
+ pk = context.primary_key.to_s.gsub(/[^A-Za-z0-9]/, '_').strip
37
+ raise NameError, "Cannot create a primary key method from #{context.primary_key.inspect}" if pk.nil? || pk.empty?
38
+ mod.class_eval("def #{pk}; return @#{pk}; end")
39
+ mod.class_eval("def #{pk}=(value); @#{pk} = value; end")
40
+ end
41
+
42
+ # These methods are added as class methods when including Modelify.
43
+ # See Modelify for more information.
44
+ module ClassMethods
45
+ # Find the object for the given id.
46
+ def fetch(id)
47
+ return context.fetch(id)
48
+ end
49
+
50
+ # Get the context instance for this class.
51
+ def context
52
+ return Context.fetch(self)
53
+ end
54
+ end
55
+
56
+ # These methods are addes as instance methods when including Modelify.
57
+ # See Modelify for more information.
58
+ module InstanceMethods
59
+
60
+ # Save the given object to persistent storage.
61
+ def save
62
+ return context.save(self)
63
+ end
64
+
65
+ # Remove the given object from persistent storage.
66
+ def remove
67
+ return context.remove(self)
68
+ end
69
+
70
+ # Return the context instance for this object.
71
+ def context
72
+ return Context.fetch(self)
73
+ end
74
+ end
75
+
76
+ # The find methods. See FindMethods::ClassMethods for details.
77
+ module FindMethods
78
+
79
+ def self.included(mod) # :nodoc:
80
+ mod.send(:extend, ClassMethods)
81
+ end
82
+
83
+ # The model find class methods.
84
+ module ClassMethods
85
+
86
+ # Calls find on the Context. See Poro::Context::FindMethods for details.
87
+ def find(arg, opts={})
88
+ return context.find(arg, opts)
89
+ end
90
+
91
+ # Calls data_store_find on the Context. See Poro::Context::FindMethods for details.
92
+ def data_store_find(first_or_all, *args, &block)
93
+ return context.data_store_find(first_or_all, *args, &block)
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ module Poro
2
+ # Include this module into any class in order to flag it for persistence.
3
+ #
4
+ # This is the only required change to a class in order to make it persistent.
5
+ # This flags it so that the factories know that it is okay to generate a
6
+ # context for this class and persist it.
7
+ #
8
+ # The only method this adds is a convenience class method for configuring
9
+ # the Context instance that backs the class it is included in.
10
+ #
11
+ # This module represents the only required breech of the hands off your code
12
+ # philosophy that Poro embodies.
13
+ #
14
+ # For those looking to add more model like behaviors, include Poro::Modelify
15
+ # as well.
16
+ module Persistify
17
+
18
+ def self.included(mod) # :nodoc:
19
+ mod.send(:extend, ClassMethods)
20
+
21
+ # Force the initialization of the context now, as one would expect it to
22
+ # exist after including this module. This also makes sure that on load
23
+ # all the contexts that are going to exist are known for introspection.
24
+ Context.fetch(mod)
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ # A convenience method to more easily call
30
+ # <tt>Context.configure_for_class</tt> from within a class decleration.
31
+ #
32
+ # This was added to ease the transition from existing model based ORMs,
33
+ # and is up for debate. It may be better to directly use
34
+ # <tt>Context.configure_for_class</tt>.
35
+ def configure_context(&configuration_block)
36
+ return Context.configure_for_class(self, &configuration_block)
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,2 @@
1
+ require 'poro/util/inflector'
2
+ require 'poro/util/module_finder'
@@ -0,0 +1,103 @@
1
+ module Poro
2
+ module Util
3
+ # = License
4
+ #
5
+ # Inflector is from AcitveSupport, which is distributed via the following
6
+ # MIT license:
7
+ #
8
+ # Copyright (c) 2005-2010 David Heinemeier Hansson
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # "Software"), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+ #
29
+ # = Overview
30
+ #
31
+ # This module contains the inflector from ActiveSupport, but in its own
32
+ # namespace and without being injected into the String class. This prevents
33
+ # Poro's implementation from clobbering that of ActiveSupport, should the
34
+ # rest of your project be using it, even if you include ActiveSupport before
35
+ # Poro.
36
+ module Inflector
37
+ # Space intentionally left blank.
38
+ end
39
+ end
40
+ end
41
+
42
+ require 'poro/util/inflector/inflections'
43
+ require 'poro/util/inflector/methods'
44
+
45
+ module Poro
46
+ module Util
47
+ Inflector.inflections do |inflect|
48
+ inflect.plural(/$/, 's')
49
+ inflect.plural(/s$/i, 's')
50
+ inflect.plural(/(ax|test)is$/i, '\1es')
51
+ inflect.plural(/(octop|vir)us$/i, '\1i')
52
+ inflect.plural(/(alias|status)$/i, '\1es')
53
+ inflect.plural(/(bu)s$/i, '\1ses')
54
+ inflect.plural(/(buffal|tomat)o$/i, '\1oes')
55
+ inflect.plural(/([ti])um$/i, '\1a')
56
+ inflect.plural(/sis$/i, 'ses')
57
+ inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
58
+ inflect.plural(/(hive)$/i, '\1s')
59
+ inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
60
+ inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
61
+ inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
62
+ inflect.plural(/([m|l])ouse$/i, '\1ice')
63
+ inflect.plural(/^(ox)$/i, '\1en')
64
+ inflect.plural(/(quiz)$/i, '\1zes')
65
+
66
+ inflect.singular(/s$/i, '')
67
+ inflect.singular(/(n)ews$/i, '\1ews')
68
+ inflect.singular(/([ti])a$/i, '\1um')
69
+ inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis')
70
+ inflect.singular(/(^analy)ses$/i, '\1sis')
71
+ inflect.singular(/([^f])ves$/i, '\1fe')
72
+ inflect.singular(/(hive)s$/i, '\1')
73
+ inflect.singular(/(tive)s$/i, '\1')
74
+ inflect.singular(/([lr])ves$/i, '\1f')
75
+ inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
76
+ inflect.singular(/(s)eries$/i, '\1eries')
77
+ inflect.singular(/(m)ovies$/i, '\1ovie')
78
+ inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
79
+ inflect.singular(/([m|l])ice$/i, '\1ouse')
80
+ inflect.singular(/(bus)es$/i, '\1')
81
+ inflect.singular(/(o)es$/i, '\1')
82
+ inflect.singular(/(shoe)s$/i, '\1')
83
+ inflect.singular(/(cris|ax|test)es$/i, '\1is')
84
+ inflect.singular(/(octop|vir)i$/i, '\1us')
85
+ inflect.singular(/(alias|status)es$/i, '\1')
86
+ inflect.singular(/^(ox)en/i, '\1')
87
+ inflect.singular(/(vert|ind)ices$/i, '\1ex')
88
+ inflect.singular(/(matr)ices$/i, '\1ix')
89
+ inflect.singular(/(quiz)zes$/i, '\1')
90
+ inflect.singular(/(database)s$/i, '\1')
91
+
92
+ inflect.irregular('person', 'people')
93
+ inflect.irregular('man', 'men')
94
+ inflect.irregular('child', 'children')
95
+ inflect.irregular('sex', 'sexes')
96
+ inflect.irregular('move', 'moves')
97
+ inflect.irregular('cow', 'kine')
98
+
99
+ inflect.uncountable(%w(equipment information rice money species series fish sheep jeans))
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,213 @@
1
+ module Poro
2
+ module Util
3
+ module Inflector
4
+ # A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional
5
+ # inflection rules. Examples:
6
+ #
7
+ # ActiveSupport::Inflector.inflections do |inflect|
8
+ # inflect.plural /^(ox)$/i, '\1\2en'
9
+ # inflect.singular /^(ox)en/i, '\1'
10
+ #
11
+ # inflect.irregular 'octopus', 'octopi'
12
+ #
13
+ # inflect.uncountable "equipment"
14
+ # end
15
+ #
16
+ # New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the
17
+ # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may
18
+ # already have been loaded.
19
+ class Inflections
20
+ def self.instance
21
+ @__instance__ ||= new
22
+ end
23
+
24
+ attr_reader :plurals, :singulars, :uncountables, :humans
25
+
26
+ def initialize
27
+ @plurals, @singulars, @uncountables, @humans = [], [], [], []
28
+ end
29
+
30
+ # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
31
+ # The replacement should always be a string that may include references to the matched data from the rule.
32
+ def plural(rule, replacement)
33
+ @uncountables.delete(rule) if rule.is_a?(String)
34
+ @uncountables.delete(replacement)
35
+ @plurals.insert(0, [rule, replacement])
36
+ end
37
+
38
+ # Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
39
+ # The replacement should always be a string that may include references to the matched data from the rule.
40
+ def singular(rule, replacement)
41
+ @uncountables.delete(rule) if rule.is_a?(String)
42
+ @uncountables.delete(replacement)
43
+ @singulars.insert(0, [rule, replacement])
44
+ end
45
+
46
+ # Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
47
+ # for strings, not regular expressions. You simply pass the irregular in singular and plural form.
48
+ #
49
+ # Examples:
50
+ # irregular 'octopus', 'octopi'
51
+ # irregular 'person', 'people'
52
+ def irregular(singular, plural)
53
+ @uncountables.delete(singular)
54
+ @uncountables.delete(plural)
55
+ if singular[0,1].upcase == plural[0,1].upcase
56
+ plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
57
+ plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1])
58
+ singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
59
+ else
60
+ plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
61
+ plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
62
+ plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
63
+ plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
64
+ singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1])
65
+ singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1])
66
+ end
67
+ end
68
+
69
+ # Add uncountable words that shouldn't be attempted inflected.
70
+ #
71
+ # Examples:
72
+ # uncountable "money"
73
+ # uncountable "money", "information"
74
+ # uncountable %w( money information rice )
75
+ def uncountable(*words)
76
+ (@uncountables << words).flatten!
77
+ end
78
+
79
+ # Specifies a humanized form of a string by a regular expression rule or by a string mapping.
80
+ # When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
81
+ # When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
82
+ #
83
+ # Examples:
84
+ # human /_cnt$/i, '\1_count'
85
+ # human "legacy_col_person_name", "Name"
86
+ def human(rule, replacement)
87
+ @humans.insert(0, [rule, replacement])
88
+ end
89
+
90
+ # Clears the loaded inflections within a given scope (default is <tt>:all</tt>).
91
+ # Give the scope as a symbol of the inflection type, the options are: <tt>:plurals</tt>,
92
+ # <tt>:singulars</tt>, <tt>:uncountables</tt>, <tt>:humans</tt>.
93
+ #
94
+ # Examples:
95
+ # clear :all
96
+ # clear :plurals
97
+ def clear(scope = :all)
98
+ case scope
99
+ when :all
100
+ @plurals, @singulars, @uncountables = [], [], []
101
+ else
102
+ instance_variable_set "@#{scope}", []
103
+ end
104
+ end
105
+ end
106
+
107
+ # Yields a singleton instance of Inflector::Inflections so you can specify additional
108
+ # inflector rules.
109
+ #
110
+ # Example:
111
+ # ActiveSupport::Inflector.inflections do |inflect|
112
+ # inflect.uncountable "rails"
113
+ # end
114
+ def inflections
115
+ if block_given?
116
+ yield Inflections.instance
117
+ else
118
+ Inflections.instance
119
+ end
120
+ end
121
+
122
+ # Returns the plural form of the word in the string.
123
+ #
124
+ # Examples:
125
+ # "post".pluralize # => "posts"
126
+ # "octopus".pluralize # => "octopi"
127
+ # "sheep".pluralize # => "sheep"
128
+ # "words".pluralize # => "words"
129
+ # "CamelOctopus".pluralize # => "CamelOctopi"
130
+ def pluralize(word)
131
+ result = word.to_s.dup
132
+
133
+ if word.empty? || inflections.uncountables.include?(result.downcase)
134
+ result
135
+ else
136
+ inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
137
+ result
138
+ end
139
+ end
140
+
141
+ # The reverse of +pluralize+, returns the singular form of a word in a string.
142
+ #
143
+ # Examples:
144
+ # "posts".singularize # => "post"
145
+ # "octopi".singularize # => "octopus"
146
+ # "sheep".singularize # => "sheep"
147
+ # "word".singularize # => "word"
148
+ # "CamelOctopi".singularize # => "CamelOctopus"
149
+ def singularize(word)
150
+ result = word.to_s.dup
151
+
152
+ if inflections.uncountables.any? { |inflection| result =~ /#{inflection}\Z/i }
153
+ result
154
+ else
155
+ inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
156
+ result
157
+ end
158
+ end
159
+
160
+ # Capitalizes the first word and turns underscores into spaces and strips a
161
+ # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
162
+ #
163
+ # Examples:
164
+ # "employee_salary" # => "Employee salary"
165
+ # "author_id" # => "Author"
166
+ def humanize(lower_case_and_underscored_word)
167
+ result = lower_case_and_underscored_word.to_s.dup
168
+
169
+ inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
170
+ result.gsub(/_id$/, "").gsub(/_/, " ").capitalize
171
+ end
172
+
173
+ # Capitalizes all the words and replaces some characters in the string to create
174
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
175
+ # used in the Rails internals.
176
+ #
177
+ # +titleize+ is also aliased as as +titlecase+.
178
+ #
179
+ # Examples:
180
+ # "man from the boondocks".titleize # => "Man From The Boondocks"
181
+ # "x-men: the last stand".titleize # => "X Men: The Last Stand"
182
+ def titleize(word)
183
+ humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
184
+ end
185
+
186
+ # Create the name of a table like Rails does for models to table names. This method
187
+ # uses the +pluralize+ method on the last word in the string.
188
+ #
189
+ # Examples
190
+ # "RawScaledScorer".tableize # => "raw_scaled_scorers"
191
+ # "egg_and_ham".tableize # => "egg_and_hams"
192
+ # "fancyCategory".tableize # => "fancy_categories"
193
+ def tableize(class_name)
194
+ pluralize(underscore(class_name))
195
+ end
196
+
197
+ # Create a class name from a plural table name like Rails does for table names to models.
198
+ # Note that this returns a string and not a Class. (To convert to an actual class
199
+ # follow +classify+ with +constantize+.)
200
+ #
201
+ # Examples:
202
+ # "egg_and_hams".classify # => "EggAndHam"
203
+ # "posts".classify # => "Post"
204
+ #
205
+ # Singular names are not handled correctly:
206
+ # "business".classify # => "Busines"
207
+ def classify(table_name)
208
+ # strip out any leading schema name
209
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
210
+ end
211
+ end
212
+ end
213
+ end