ru.Bee 1.1.32 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03808642660607dcc9ecbd0f6f2fbcfc510e48f28e4eb583a0aace626642142f'
4
- data.tar.gz: 11156690b45851b45f1ebeae3dff18ee8c9b44e2f3bef0a84d63a123f5e603a6
3
+ metadata.gz: a01285afa9a2c3b5f9a08cd5cbd58ddbd300181a1cc4c5b823710c873e84300a
4
+ data.tar.gz: 241e42bbccbfe0731d83b80d58f5d40f0969c003b067660e53ace470294f2896
5
5
  SHA512:
6
- metadata.gz: 5b48a1ccc2b3c7c9f7a5eb9492f8fca77488d842599d1a38a6d64f2f2534e41d1e69d69de7aa6d045935c98578ad9b77b36ccedbbd72f3b4086b8855e8e87b4d
7
- data.tar.gz: 0c7067e04ddb2039b84a734a29ed743dcd187ea0ff8a21d076bd9c1c06d6413105c1c631b398c35186ee84c5a2d08c011cb24ed6b80bba0de57a3379f1d54d1b
6
+ metadata.gz: 2dbdeed01d5187c61ebbc3bc5773ce9d6150f6952fe634e76d4c8f22718b3c14091f591b879e9ecf94954672d8f9719ce1994f490966cc2d79aa899baca8e994
7
+ data.tar.gz: 91eaf06432d1f35f1c1450ea0af9bac800194bfc448bd055e7455294170682965f9e25122e2a6e64d9587049dbe6b91c6b062c6a345381cd11c3f22bb98fd60d
data/bin/rubee CHANGED
@@ -160,7 +160,7 @@ elsif command == "test"
160
160
  color_puts "Running all tests ...", color: :yellow
161
161
  exec("ruby -Itest -e \"Dir.glob('.#{lib}/tests/**/*_test.rb').each { |file| require file }\"")
162
162
  end
163
- elsif ['generate', 'g'].include? command
163
+ elsif ['generate', 'gen'].include? command
164
164
  method, path = ARGV[1..2]
165
165
  ENV['RACK_ENV'] ||= 'development'
166
166
 
@@ -181,9 +181,9 @@ elsif command == "db"
181
181
  command, file_name = ARGV[1]&.split(':')
182
182
  if Rubee::PROJECT_NAME == 'rubee'
183
183
  Rubee::Configuration.setup(env=:test) do |config|
184
- config.database_url = { url: "sqlite://test.db", env: }
184
+ config.database_url = { url: "sqlite://lib/tests/test.db", env: }
185
185
  end
186
- Rubee::SequelObject.reconnect!
186
+ Rubee::SequelObject.reconnect! unless command == 'init'
187
187
  end
188
188
 
189
189
  def ensure_database_exists(db_url)
@@ -193,7 +193,7 @@ elsif command == "db"
193
193
  begin
194
194
  Sequel.connect(db_url)
195
195
  color_puts "Database #{ENV['RACK_ENV']} exists", color: :cyan
196
- rescue => _
196
+ rescue Exception => e
197
197
  if File.exist?(db_path = db_url.sub(/^sqlite:\/\//, ''))
198
198
  color_puts "Database #{ENV['RACK_ENV']} exists", color: :cyan
199
199
  else
@@ -237,14 +237,22 @@ elsif command == "db"
237
237
  end
238
238
 
239
239
 
240
- if command == 'run'
240
+ if command == 'run'
241
241
  Rubee::Autoload.call
242
+ file_names = if file_name == 'all'
243
+ lib = Rubee::PROJECT_NAME == 'rubee' ? '/lib' : ''
244
+ Dir.glob(".#{lib}/db/*.rb").map { |file| File.basename(file, '.rb') }.select { |file| file != 'structure' }
245
+ else
246
+ [file_name]
247
+ end
242
248
  Rubee::Configuration.envs.each do |env|
243
249
  ENV['RACK_ENV'] = env.to_s
244
- color_puts "Run #{file_name} file for #{env} env", color: :cyan
245
- Object.const_get(file_name.split('_').map(&:capitalize).join).new.call
250
+ file_names.each do |file_name|
251
+ color_puts "Run #{file_name} file for #{env} env", color: :cyan
252
+ Object.const_get(file_name.split('_').map(&:capitalize).join).new.call
253
+ end
246
254
  end
247
- color_puts "Migration #{file_name} completed", color: :green
255
+ color_puts "Migration for #{file_name} completed", color: :green
248
256
  unless Rubee::PROJECT_NAME == 'rubee'
249
257
  color_puts "Regenerate schema file", color: :cyan
250
258
  generate_structure
@@ -261,15 +269,25 @@ elsif ['console'].include? command
261
269
  ENV['RACK_ENV'] ||= 'development'
262
270
 
263
271
  Rubee::Autoload.call
272
+ if Rubee::PROJECT_NAME == 'rubee'
273
+ Rubee::Configuration.setup(env=:test) do |config|
274
+ config.database_url = { url: "sqlite://lib/tests/test.db", env: }
275
+ end
276
+ Rubee::Autoload.call
277
+ Rubee::SequelObject.reconnect!
278
+ end
264
279
 
265
280
  def reload
266
- app_files = Dir["./#{APP_ROOT}/**/*.rb"]
281
+ app_files = Dir["./#{Rubee::APP_ROOT}/**/*.rb"]
267
282
  app_files.each { |file| load file }
268
283
  color_puts "Reloaded ..", color: :green
269
284
  end
270
-
271
- # Start IRB
272
- IRB.start
285
+ begin
286
+ # Start IRB
287
+ IRB.start
288
+ rescue Exception => e
289
+ IRB.start
290
+ end
273
291
  else
274
292
  color_puts "Unknown command: #{command}", color: :red
275
293
  end
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= @title || '' %></title>
7
+ <style>
8
+ body {
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ height: 100vh;
13
+ margin: 0;
14
+ background-color: #fdf6a5;
15
+ text-align: center;
16
+ font-family: Arial, sans-serif;
17
+ }
18
+ .container {
19
+ padding: 20px;
20
+ background: white;
21
+ border-radius: 10px;
22
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
23
+ }
24
+ img {
25
+ width: 50px;
26
+ height: auto;
27
+ margin-top: 10px;
28
+ }
29
+ h1 {
30
+ font-size: 1.8rem;
31
+ margin: 10px 0;
32
+ }
33
+ @media (max-width: 600px) {
34
+ h1 {
35
+ font-size: 1.5rem;
36
+ }
37
+ img {
38
+ width: 60px;
39
+ }
40
+ }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <%= _yield_template %>
45
+ </body>
46
+ </html>
@@ -0,0 +1 @@
1
+ <h1>All set up and running!</h1>
@@ -1,52 +1,14 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Setup Complete</title>
7
- <style>
8
- body {
9
- display: flex;
10
- justify-content: center;
11
- align-items: center;
12
- height: 100vh;
13
- margin: 0;
14
- background-color: #fdf6a5;
15
- text-align: center;
16
- font-family: Arial, sans-serif;
17
- }
18
- .container {
19
- padding: 20px;
20
- background: white;
21
- border-radius: 10px;
22
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
23
- }
24
- img {
25
- width: 50px;
26
- height: auto;
27
- margin-top: 10px;
28
- }
29
- h1 {
30
- font-size: 1.8rem;
31
- margin: 10px 0;
32
- }
33
- @media (max-width: 600px) {
34
- h1 {
35
- font-size: 1.5rem;
36
- }
37
- img {
38
- width: 60px;
39
- }
40
- }
41
- </style>
42
- </head>
43
- <body>
4
+
44
5
  <div class="container">
45
- <h1>All set up and running!</h1>
6
+ <%= render_template :welcome_header %>
46
7
  <br/>
47
8
  <img src="images/rubee.svg" alt="ruBee">
48
9
  <br/>
49
10
  <p>rubee homepage: <a href="https://github.com/nucleom42/rubee"><br/>https://github.com/nucleom42/rubee</a></p>
11
+ <br/>
12
+ <p>Version: <%= Rubee::VERSION %></p>
50
13
  </div>
51
- </body>
52
- </html>
14
+
@@ -0,0 +1,14 @@
1
+ class CreateAccounts
2
+ def call
3
+ unless Rubee::SequelObject::DB.tables.include?(:accounts)
4
+ Rubee::SequelObject::DB.create_table :accounts do
5
+ primary_key :id
6
+ String :addres
7
+ foreign_key :user_id, :users
8
+ end
9
+
10
+ Account.create(addres: "13th Ave, NY", user_id: User.all.first.id)
11
+ Account.create(addres: "14th Ave, NY", user_id: User.all.last.id)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ class CreateComments
2
+ def call
3
+ unless Rubee::SequelObject::DB.tables.include?(:comments)
4
+ Rubee::SequelObject::DB.create_table :comments do
5
+ primary_key :id
6
+ String :text
7
+ Integer :user_id
8
+ end
9
+
10
+ User.create(email: "ok@ok.com", password: "password")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ class CreatePosts
2
+ def call
3
+ unless Rubee::SequelObject::DB.tables.include?(:posts)
4
+ Rubee::SequelObject::DB.create_table :posts do
5
+ primary_key :id
6
+ foreign_key :user_id, :users
7
+ foreign_key :comment_id, :comments
8
+ end
9
+
10
+ Post.create(user_id: User.all.first.id, comment_id: Comment.all.first.id)
11
+ Post.create(user_id: User.all.last.id, comment_id: Comment.all.last.id)
12
+ end
13
+ end
14
+ end
data/lib/db/structure.rb CHANGED
@@ -30,5 +30,97 @@ STRUCTURE = {
30
30
  ruby_default: nil,
31
31
  max_length: 255
32
32
  }
33
+ },
34
+ accounts: {
35
+ id: {
36
+ generated: false,
37
+ allow_null: false,
38
+ default: nil,
39
+ db_type: "INTEGER",
40
+ primary_key: true,
41
+ auto_increment: true,
42
+ type: "integer",
43
+ ruby_default: nil
44
+ },
45
+ addres: {
46
+ generated: false,
47
+ allow_null: true,
48
+ default: nil,
49
+ db_type: "varchar(255)",
50
+ primary_key: false,
51
+ type: "string",
52
+ ruby_default: nil,
53
+ max_length: 255
54
+ },
55
+ user_id: {
56
+ generated: false,
57
+ allow_null: true,
58
+ default: nil,
59
+ db_type: "INTEGER",
60
+ primary_key: false,
61
+ type: "integer",
62
+ ruby_default: nil
63
+ }
64
+ },
65
+ posts: {
66
+ id: {
67
+ generated: false,
68
+ allow_null: false,
69
+ default: nil,
70
+ db_type: "INTEGER",
71
+ primary_key: true,
72
+ auto_increment: true,
73
+ type: "integer",
74
+ ruby_default: nil
75
+ },
76
+ user_id: {
77
+ generated: false,
78
+ allow_null: true,
79
+ default: nil,
80
+ db_type: "INTEGER",
81
+ primary_key: false,
82
+ type: "integer",
83
+ ruby_default: nil
84
+ },
85
+ comment_id: {
86
+ generated: false,
87
+ allow_null: true,
88
+ default: nil,
89
+ db_type: "INTEGER",
90
+ primary_key: false,
91
+ type: "integer",
92
+ ruby_default: nil
93
+ }
94
+ },
95
+ comments: {
96
+ id: {
97
+ generated: false,
98
+ allow_null: false,
99
+ default: nil,
100
+ db_type: "INTEGER",
101
+ primary_key: true,
102
+ auto_increment: true,
103
+ type: "integer",
104
+ ruby_default: nil
105
+ },
106
+ text: {
107
+ generated: false,
108
+ allow_null: true,
109
+ default: nil,
110
+ db_type: "varchar(255)",
111
+ primary_key: false,
112
+ type: "string",
113
+ ruby_default: nil,
114
+ max_length: 255
115
+ },
116
+ user_id: {
117
+ generated: false,
118
+ allow_null: true,
119
+ default: nil,
120
+ db_type: "INTEGER",
121
+ primary_key: false,
122
+ type: "integer",
123
+ ruby_default: nil
124
+ }
33
125
  }
34
126
  }
@@ -1,5 +1,5 @@
1
1
  unless defined?(Rubee)
2
- require_relative '../../../../ee.rb'
2
+ require_relative '../../../../rubee.rb'
3
3
  Rubee::Autoload.call
4
4
  end
5
5
 
@@ -18,7 +18,7 @@ module Rubee
18
18
  end
19
19
  end
20
20
 
21
- def response_with type: nil, object: nil, status: 200, mime_type: nil, render_view: nil, headers: {}, to: nil, file: nil, filename: nil
21
+ def response_with type: nil, object: nil, status: 200, mime_type: nil, render_view: nil, headers: {}, to: nil, file: nil, filename: nil, **options
22
22
  case type&.to_sym
23
23
  in :json
24
24
  rendered_json = object.is_a?(Array) ? object&.map(&:to_h).to_json : object.to_json
@@ -42,13 +42,31 @@ module Rubee
42
42
  return [302, headers.merge("location" => "#{to}"), ["Unauthentificated"]]
43
43
  else # rendering erb view is a default behavior
44
44
  view_file_name = self.class.name.split("Controller").first.downcase
45
- erb_file = render_view ? "#{render_view}.erb" : "#{view_file_name}_#{@route[:action]}.erb"
45
+ erb_file = render_view ? "#{render_view}" : "#{view_file_name}_#{@route[:action]}"
46
46
  lib = Rubee::PROJECT_NAME == 'rubee' ? 'lib/' : ''
47
- rendered_erb = ERB.new(File.open("#{lib}app/views/#{erb_file}").read).result(binding)
48
- return [status, headers.merge("content-type" => "text/html"), [rendered_erb]]
47
+ view = render_template(erb_file, { object:, **(options[:locals] || {}) })
48
+
49
+ whole_erb = if File.exist?(layout_path = "#{lib}app/views/#{options[:layout] || 'layout'}.erb")
50
+ context = Object.new
51
+ context.define_singleton_method(:_yield_template) { view }
52
+ layout = File.read(layout_path)
53
+ ERB.new(layout).result(context.instance_eval { binding })
54
+ else
55
+ ERB.new(view).result(binding)
56
+ end
57
+
58
+ return [status, headers.merge("content-type" => "text/html"), [whole_erb]]
49
59
  end
50
60
  end
51
61
 
62
+ def render_template(file_name, locals = {})
63
+ lib = Rubee::PROJECT_NAME == 'rubee' ? 'lib/' : ''
64
+ path = "#{lib}app/views/#{file_name}.erb"
65
+ erb_template = ERB.new(File.read(path))
66
+
67
+ erb_template.result(binding)
68
+ end
69
+
52
70
  def params
53
71
  inputs = @request.env['rack.input'].read
54
72
  body = JSON.parse(@request.body.read.strip) rescue body = {}
@@ -0,0 +1,52 @@
1
+ module Rubee
2
+ module DatabaseObjectable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.include InstanceMethods
6
+ base.prepend Initializer
7
+
8
+ base.include Rubee::Hookable
9
+ base.include Rubee::Serializable
10
+ end
11
+
12
+ module ClassMethods
13
+ def pluralize_class_name
14
+ pluralize(self.name.downcase)
15
+ end
16
+
17
+ def pluralize(word)
18
+ if word.end_with?('y') && !%w[a e i o u].include?(word[-2])
19
+ word[0..-2] + 'ies' # Replace "y" with "ies"
20
+ elsif word.end_with?('s', 'x', 'z', 'ch', 'sh')
21
+ word + 'es' # Add "es" for certain endings
22
+ else
23
+ word + 's' # Default to adding "s"
24
+ end
25
+ end
26
+
27
+ def singularize(word)
28
+ if word.end_with?('ies') && word.length > 3
29
+ word[0..-4] + 'y' # Convert "ies" to "y"
30
+ elsif word.end_with?('es') && %w[s x z ch sh].any? { |ending| word[-(ending.length + 2)..-3] == ending }
31
+ word[0..-3] # Remove "es" for words like "foxes", "buses"
32
+ elsif word.end_with?('s') && word.length > 1
33
+ word[0..-2] # Remove "s" for regular plurals
34
+ else
35
+ word # Return as-is if no plural form is detected
36
+ end
37
+ end
38
+
39
+ def accessor_names
40
+ instance_methods(false)
41
+ .select { |m| method_defined?("#{m}=") } # Check if setter exists
42
+ end
43
+ end
44
+
45
+ module InstanceMethods
46
+ end
47
+
48
+ module Initializer
49
+ end
50
+ end
51
+ end
52
+
@@ -1,9 +1,21 @@
1
1
  module Rubee
2
- class SequelObject < DatabaseObject
3
- DB = Sequel.connect(Rubee::Configuration.get_database_url) rescue nil
4
-
5
- def destroy
6
- self.class.connection.where(id:).delete
2
+ class SequelObject
3
+ include Rubee::DatabaseObjectable
4
+
5
+ def destroy(cascade: false, **options)
6
+ if cascade
7
+ # find all tables with foreign key
8
+ tables_with_fk = DB.tables.select do |table|
9
+ DB.foreign_key_list(table).any? { |fk| fk[:table] == self.class.pluralize_class_name.to_sym }
10
+ end
11
+ # destroy related records
12
+ tables_with_fk.each do |table|
13
+ fk_name ||= "#{self.class.name.to_s.downcase}_id".to_sym
14
+ target_klass = Object.const_get(self.class.singularize(table.to_s).capitalize)
15
+ target_klass.where(fk_name => id).map(&:destroy)
16
+ end
17
+ end
18
+ self.class.dataset.where(id:).delete
7
19
  end
8
20
 
9
21
  def save
@@ -28,7 +40,7 @@ module Rubee
28
40
 
29
41
  def update(args = {})
30
42
  assign_attributes(args)
31
- found_hash = self.class.connection.where(id:)
43
+ found_hash = self.class.dataset.where(id:)
32
44
  return self.class.find(id) if found_hash&.update(**args)
33
45
 
34
46
  false
@@ -44,46 +56,122 @@ module Rubee
44
56
 
45
57
  class << self
46
58
  def last
47
- found_hash = connection.order(:id).last
59
+ found_hash = dataset.order(:id).last
60
+ return self.new(**found_hash) if found_hash
61
+
62
+ nil
63
+ end
64
+
65
+ def first
66
+ found_hash = dataset.order(:id).first
48
67
  return self.new(**found_hash) if found_hash
49
68
 
50
69
  nil
51
70
  end
52
71
 
72
+ # ## User
73
+ # owns_many :comments
74
+ # > user.comments
75
+ # > [<comment1>, <comment2>]
76
+ def owns_many(assoc, fk_name: nil, over: nil, **options)
77
+ singularized_assoc_name = singularize(assoc.to_s)
78
+ fk_name ||= "#{self.name.to_s.downcase}_id"
79
+
80
+ define_method(assoc) do
81
+ klass = Object.const_get(singularized_assoc_name.capitalize)
82
+ if over
83
+ sequel_dataset = klass
84
+ .join(over.to_sym, "#{singularized_assoc_name}_id".to_sym => :id)
85
+ .where(fk_name.to_sym => id)
86
+ self.class.serialize(sequel_dataset, klass)
87
+ else
88
+ klass.where(fk_name.to_sym => id)
89
+ end
90
+ end
91
+ end
92
+
93
+ # ## Comment
94
+ # owns_one :user
95
+ # > comment.user
96
+ # > <user>
97
+ def owns_one(assoc, options={})
98
+ Sequel::Model.one_to_one(assoc, **options)
99
+ fk_name ||= "#{self.name.to_s.downcase}_id"
100
+ define_method(assoc) do
101
+ Object.const_get(assoc.capitalize).where(fk_name.to_sym => id)&.first
102
+ end
103
+ end
104
+
105
+ # ## Account
106
+ # holds :user
107
+ # > account.user
108
+ # > <user>
109
+ def holds(assoc, fk_name: nil, **options)
110
+ fk_name ||= "#{assoc.to_s.downcase}_id"
111
+ define_method(assoc) do
112
+ target_klass = Object.const_get(assoc.capitalize)
113
+ target_klass.find(self.send(fk_name))
114
+ end
115
+ end
116
+
53
117
  def reconnect!
118
+ return if defined?(DB) && !DB.nil?
119
+
54
120
  const_set(:DB, Sequel.connect(Rubee::Configuration.get_database_url))
55
121
  end
56
122
 
57
- def connection
58
- @connection ||= DB[pluralize_class_name.to_sym]
123
+ def dataset
124
+ @dataset ||= DB[pluralize_class_name.to_sym]
125
+ rescue Exception => _
126
+ reconnect!
127
+ retry
59
128
  end
60
129
 
61
130
  def all
62
- connection.map do |record_hash|
131
+ dataset.map do |record_hash|
63
132
  self.new(**record_hash)
64
133
  end
65
134
  end
66
135
 
67
136
  def find(id)
68
- found_hash = connection.where(id:)&.first
137
+ found_hash = dataset.where(id:)&.first
69
138
  return self.new(**found_hash) if found_hash
70
139
 
71
140
  nil
72
141
  end
73
142
 
74
143
  def where(args)
75
- connection.where(**args).map do |record_hash|
144
+ dataset.where(**args).map do |record_hash|
145
+ self.new(**record_hash)
146
+ end
147
+ end
148
+
149
+ def order(*args)
150
+ dataset.order(*args).map do |record_hash|
76
151
  self.new(**record_hash)
77
152
  end
78
153
  end
79
154
 
155
+ def join(assoc, args)
156
+ dataset.join(assoc, **args)
157
+ end
158
+
80
159
  def create(attrs)
81
- out_id = connection.insert(**attrs)
160
+ out_id = dataset.insert(**attrs)
82
161
  self.new(**(attrs.merge(id: out_id)))
83
162
  end
84
163
 
85
- def destroy_all
86
- all.each(&:destroy)
164
+ def destroy_all(cascade: false)
165
+ all.each{ |record| record.destroy(cascade:) }
166
+ end
167
+
168
+ def serialize(suquel_dataset, klass = nil)
169
+ klass ||= self
170
+ suquel_dataset.map do |record_hash|
171
+ target_klass_fields = DB[pluralize(klass.name.downcase).to_sym].columns
172
+ klass_attributes = record_hash.filter{ target_klass_fields.include? _1 }
173
+ klass.new(**klass_attributes)
174
+ end
87
175
  end
88
176
  end
89
177
  end
data/lib/rubee.rb CHANGED
@@ -8,7 +8,7 @@ module Rubee
8
8
  APP_ROOT = File.expand_path(Dir.pwd) unless defined?(APP_ROOT)
9
9
  IMAGE_DIR = File.join(APP_ROOT, 'images') unless defined?(IMAGE_DIR)
10
10
  PROJECT_NAME = File.basename(APP_ROOT) unless defined?(PROJECT_NAME)
11
- VERSION = '1.1.32'
11
+ VERSION = '1.2.0'
12
12
 
13
13
  class Application
14
14
  include Singleton
@@ -123,6 +123,8 @@ module Rubee
123
123
  # autoload all rbs
124
124
  root_directory = File.dirname(__FILE__)
125
125
  priority_order_require(root_directory, black_list)
126
+ # ensure sequel object is connected
127
+ Rubee::SequelObject.reconnect!
126
128
 
127
129
  Dir.glob(File.join(APP_ROOT, '**', '*.rb')).sort.each do |file|
128
130
  base_name = File.basename(file)
@@ -149,6 +151,15 @@ module Rubee
149
151
  # app config and routes
150
152
  lib = PROJECT_NAME == 'rubee' ? 'lib/' : ''
151
153
  require_relative File.join(APP_ROOT, lib, "config/base_configuration") unless black_list.include?('base_configuration.rb')
154
+ # This is necessary prerequisitedb init step
155
+ unless defined?(Rubee::SequelObject::DB)
156
+ if PROJECT_NAME == 'rubee'
157
+ Rubee::Configuration.setup(env=:test) do |config|
158
+ config.database_url = { url: "sqlite://lib/tests/test.db", env: }
159
+ end
160
+ end
161
+ end
162
+
152
163
  require_relative File.join(APP_ROOT, lib, "config/routes") unless black_list.include?('routes.rb')
153
164
  # rubee extensions
154
165
  Dir[File.join(root_directory, "rubee/extensions/**", '*.rb')].each do |file|
@@ -163,7 +174,7 @@ module Rubee
163
174
  end
164
175
  require_relative File.join(root_directory, "rubee/controllers/base_controller") unless black_list.include?('base_controller.rb')
165
176
  # rubee models
166
- require_relative File.join(root_directory, "rubee/models/database_object") unless black_list.include?('database_object.rb')
177
+ require_relative File.join(root_directory, "rubee/models/database_objectable") unless black_list.include?('database_objectable.rb')
167
178
  require_relative File.join(root_directory, "rubee/models/sequel_object") unless black_list.include?('sequel_object.rb')
168
179
  end
169
180
  end
@@ -0,0 +1,19 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe 'Account model' do
4
+ describe 'holds :user' do
5
+ after do
6
+ Account.destroy_all cascade: true
7
+ end
8
+
9
+ describe 'when it holds user_id' do
10
+ it 'returns associated User record' do
11
+ user = User.new(email: "ok-test@test.com", password: "123")
12
+ user.save
13
+ account = Account.new(user_id: user.id, addres: "test")
14
+ account.save
15
+ _(account.user.id).must_equal user.id
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe 'Comment model' do
4
+ describe 'owns_many :users, over: :posts' do
5
+ before do
6
+ comment = Comment.new(text: "test")
7
+ comment.save
8
+ user = User.new(email: "ok-test@test.com", password: "123")
9
+ user.save
10
+ post = Post.new(user_id: user.id, comment_id: comment.id)
11
+ post.save
12
+ end
13
+
14
+ after do
15
+ Comment.destroy_all cascade: true
16
+ end
17
+
18
+ describe 'when there are associated comment records' do
19
+ it 'returns all records' do
20
+ _(Comment.where(text: "test").last.users.count).must_equal 1
21
+ _(Comment.where(text: "test").last.users.first.email).must_equal "ok-test@test.com"
22
+ end
23
+ end
24
+
25
+ describe 'sequel dataset query' do
26
+ it 'returns all records' do
27
+ result = Comment.dataset.join(:posts, comment_id: :id)
28
+ .where(comment_id: Comment.where(text: "test").last.id)
29
+ .then { |dataset| Comment.serialize(dataset) }
30
+
31
+ _(result.first.text).must_equal "test"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ class Account < Rubee::SequelObject
2
+ attr_accessor :id, :addres, :user_id
3
+ holds :user
4
+ end
@@ -0,0 +1,4 @@
1
+ class Comment < Rubee::SequelObject
2
+ attr_accessor :id, :text, :user_id
3
+ owns_many :users, over: :posts
4
+ end
@@ -0,0 +1,5 @@
1
+ class Post < Rubee::SequelObject
2
+ attr_accessor :id, :user_id, :comment_id
3
+ holds :comment
4
+ holds :user
5
+ end
@@ -0,0 +1,4 @@
1
+ class User < Rubee::SequelObject
2
+ attr_accessor :id, :email, :password
3
+ owns_many :accounts, cascade: true
4
+ end
Binary file
@@ -7,7 +7,7 @@ require_relative '../../lib/rubee'
7
7
 
8
8
  Rubee::Autoload.call
9
9
  Rubee::Configuration.setup(env=:test) do |config|
10
- config.database_url = { url: "sqlite://test.db", env: }
10
+ config.database_url = { url: "sqlite://lib/tests/test.db", env: }
11
11
  end
12
12
  Rubee::SequelObject.reconnect!
13
13
 
@@ -3,7 +3,7 @@ require_relative 'test_helper'
3
3
  describe 'User model' do
4
4
  describe ".create" do
5
5
  after do
6
- User.destroy_all
6
+ User.destroy_all cascade: true
7
7
  end
8
8
 
9
9
  describe 'when data is valid' do
@@ -26,7 +26,7 @@ describe 'User model' do
26
26
 
27
27
  describe '.save' do
28
28
  after do
29
- User.destroy_all
29
+ User.destroy_all cascade: true
30
30
  end
31
31
 
32
32
  describe 'when data is valid' do
@@ -57,4 +57,174 @@ describe 'User model' do
57
57
  end
58
58
  end
59
59
  end
60
+
61
+ describe '.update' do
62
+ after do
63
+ User.destroy_all cascade: true
64
+ end
65
+
66
+ describe 'when data is valid' do
67
+ it 'persists to db' do
68
+ user = User.new(email: "ok-test@test.com", password: "123")
69
+ user.save
70
+ user.update(password: "1234")
71
+
72
+ _(user.reload.password).must_equal "1234"
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '.destroy' do
78
+ after do
79
+ User.destroy_all cascade: true
80
+ end
81
+
82
+ describe 'when there is no related recrods' do
83
+ it 'delete the record' do
84
+ user = User.new(email: "ok-test@test.com", password: "123")
85
+ user.save
86
+ user.destroy
87
+
88
+ assert_nil user.reload
89
+ end
90
+ end
91
+
92
+ describe 'when there are related recrods' do
93
+ it 'does not delete the record' do
94
+ user = User.new(email: "ok-test@test.com", password: "123")
95
+ user.save
96
+ Account.new(user_id: user.id, addres: "test").save
97
+ user.destroy rescue nil
98
+
99
+ _(user.reload.id).must_equal user.id
100
+ end
101
+ end
102
+
103
+ describe 'when there are related recrods but passed cascade=true' do
104
+ it 'deletes the record' do
105
+ user = User.new(email: "ok-test@test.com", password: "123")
106
+ user.save
107
+ Account.new(user_id: user.id, addres: "test").save
108
+ user.destroy cascade: true
109
+
110
+ assert_nil user.reload
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '.find' do
116
+ after do
117
+ User.destroy_all cascade: true
118
+ end
119
+
120
+ describe 'when trhere is a record' do
121
+ it 'returns a record' do
122
+ user = User.new(email: "ok-test@test.com", password: "123")
123
+ user.save
124
+ _(User.find(user.id).email).must_equal user.email
125
+ end
126
+ end
127
+
128
+ describe 'when there is no record' do
129
+ it 'returns nil' do
130
+ assert_nil User.find(1)
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '.all' do
136
+ after do
137
+ User.destroy_all cascade: true
138
+ end
139
+
140
+ describe 'when there are records' do
141
+ it 'returns all records' do
142
+ user = User.new(email: "ok-test@test.com", password: "123")
143
+ user2 = User.new(email: "ok-test2@test.com", password: "123")
144
+ user.save
145
+ user2.save
146
+ _(User.all.count).must_equal 2
147
+ end
148
+ end
149
+ end
150
+
151
+ describe '.where' do
152
+ after do
153
+ User.destroy_all cascade: true
154
+ end
155
+
156
+ describe 'when there are records' do
157
+ it 'returns all records' do
158
+ user = User.new(email: "ok-test@test.com", password: "123")
159
+ user2 = User.new(email: "ok-test2@test.com", password: "123")
160
+ user.save
161
+ user2.save
162
+ _(User.where(email: "ok-test2@test.com").count).must_equal 1
163
+ end
164
+ end
165
+ end
166
+
167
+ describe '.first' do
168
+ after do
169
+ User.destroy_all cascade: true
170
+ end
171
+
172
+ describe 'when there are records' do
173
+ it 'returns first record' do
174
+ user = User.new(email: "ok-test@test.com", password: "123")
175
+ user2 = User.new(email: "ok-test2@test.com", password: "123")
176
+ user.save
177
+ user2.save
178
+ _(User.first.email).must_equal user.email
179
+ end
180
+ end
181
+ end
182
+
183
+ describe '.last' do
184
+ after do
185
+ User.destroy_all cascade: true
186
+ end
187
+
188
+ describe 'when there are records' do
189
+ it 'returns last record' do
190
+ user = User.new(email: "ok-test@test.com", password: "123")
191
+ user2 = User.new(email: "ok-test2@test.com", password: "123")
192
+ user.save
193
+ user2.save
194
+ _(User.last.email).must_equal user2.email
195
+ end
196
+ end
197
+ end
198
+
199
+ describe '.order' do
200
+ after do
201
+ User.destroy_all cascade: true
202
+ end
203
+
204
+ describe 'when there are records' do
205
+ it 'returns ordered records' do
206
+ user = User.new(email: "abc@test.com", password: "123")
207
+ user2 = User.new(email: "defg@test.com", password: "123")
208
+ user.save
209
+ user2.save
210
+ _(User.order(:email).first.email).must_equal user.email
211
+ end
212
+ end
213
+ end
214
+
215
+ describe 'owns_many' do
216
+ after do
217
+ User.destroy_all cascade: true
218
+ end
219
+
220
+ describe 'when there are associated account records' do
221
+ it 'returns all records' do
222
+ user = User.new(email: "ok-test@test.com", password: "123")
223
+ user.save
224
+ account = Account.new(user_id: user.id, addres: "test")
225
+ account.save
226
+ _(user.accounts.count).must_equal 1
227
+ end
228
+ end
229
+ end
60
230
  end
data/readme.md CHANGED
@@ -6,11 +6,11 @@
6
6
  ![GitHub Repo stars](https://img.shields.io/github/stars/nucleom42/rubee?style=social)
7
7
 
8
8
 
9
- # <img src="lib/images/rubee.svg" alt="ru.Bee" height="40"> ... ru.Bee
9
+ # <img src="lib/images/rubee.svg" alt="ruBee" height="40"> ... ruBee
10
10
 
11
- ru.Bee is a fast and lightweight Ruby application server designed for minimalism and flexibility .
11
+ ruBee is a fast and lightweight Ruby application server designed for minimalism and flexibility .
12
12
 
13
- The main philosophy of ru.Bee is to focus on Ruby language explicit implementation of the MVC web application.
13
+ The main philosophy of ruBee is to focus on Ruby language explicit implementation of the MVC web application.
14
14
 
15
15
  Want to get a quick API server up and runing? You can do it for real quick!
16
16
  <br />
@@ -27,7 +27,7 @@ All greaet features are yet to come!
27
27
  - **Router**: Router driven - generates all required files from the routes.
28
28
  - **Databases**: Sqlite3, Postgres, Mysql and many more supported by sequel gem.
29
29
  - **Views**: Json, ERB and plain HTML
30
- - **Bundlable** Charge your ru.Bee with any gem you need and update your project with bundle.
30
+ - **Bundlable** Charge your ruBee with any gem you need and update your project with bundle.
31
31
  - **ORM** All models are natively ORM objects, however you can use it as a blueurpint for any datasources.
32
32
  - **Authentificatable** Add JWT authentification easily to any controller action.
33
33
  - **Hooks** Add logic before, after and around any action.
@@ -36,7 +36,7 @@ All greaet features are yet to come!
36
36
 
37
37
  ## Installation
38
38
 
39
- 1. Install ru.Bee
39
+ 1. Install ruBee
40
40
  ```bash
41
41
  gem install ru.Bee
42
42
  ```
@@ -50,7 +50,7 @@ cd my_project
50
50
  3. Install dependencies
51
51
 
52
52
  ***Prerequisites***<br />
53
- **ru.Bee** is using **Sqlite** as a default database. However you can pick up any other database supported by sequel gem.
53
+ **ruBee** is using **Sqlite** as a default database. However you can pick up any other database supported by sequel gem.
54
54
  Aside that, make sure:
55
55
  **Ruby** language (3+) is installed
56
56
  **Bundler** is installed
@@ -59,7 +59,7 @@ Aside that, make sure:
59
59
  bundle install
60
60
  ```
61
61
 
62
- 4. Run ru.Bee server. Default port is 7000
62
+ 4. Run ruBee server. Default port is 7000
63
63
  ```bash
64
64
  rubee start
65
65
  ```
@@ -97,7 +97,7 @@ rubee generate get /apples
97
97
  4. Fill those files with the logic you need and run the server again!
98
98
 
99
99
  ## Model
100
- Model in ru.Bee is just simple ruby object that can be serilalized in the view
100
+ Model in ruBee is just simple ruby object that can be serilalized in the view
101
101
  in the way it required (ie json).
102
102
 
103
103
  Here below is a simple example on how it can be used by rendering json from in memory object
@@ -107,7 +107,7 @@ Here below is a simple example on how it can be used by rendering json from in m
107
107
 
108
108
  def show
109
109
  # in memory example
110
- apples = [Apple.new(colour: 'red', weight: '1lb'), Apple.new(colour: 'green', weight: '1lb')]
110
+ apples = [Apple.new(colour: 'red', weight: '1lb'), Apple.new(colour: 'green', weight: '1lb')]
111
111
  apple = apples.find { |apple| apple.colour = params[:colour] }
112
112
 
113
113
  response_with object: apple, type: :json
@@ -145,12 +145,144 @@ So in the controller you would need to query your target object now.
145
145
  end
146
146
  ```
147
147
 
148
+ #### Rubee::SequelObject base methods:
149
+
150
+ Initiate new record in memory
151
+ ```Ruby
152
+ irb(main):015> user = User.new(email: "llo@ok.com", password: 543)
153
+ => #<User:0x000000010cda23b8 @email="llo@ok.com", @password=543>
154
+ ```
155
+
156
+ Save record in db
157
+ ```Ruby
158
+ => #<User:0x000000010cda23b8 @email="llo@ok.com", @password=543>
159
+ irb(main):018> user.save
160
+ => true
161
+ ```
162
+
163
+ Update record with new value
164
+ ```Ruby
165
+ irb(main):019> user.update(email: "update@email.com")
166
+ => #<User:0x000000010c39b298 @email="update@email.com", @id=3, @password="543">
167
+ ```
168
+
169
+ Check whether it includes id
170
+ ```Ruby
171
+ irb(main):015> user = User.new(email: "llo@ok.com", password: 543)
172
+ => #<User:0x000000010cda23b8 @email="llo@ok.com", @password=543>
173
+ irb(main):016> user.persisted?
174
+ => false
175
+ ```
176
+
177
+ Get the record from the database
178
+ ```Ruby
179
+ irb(main):011> user = User.last
180
+ => #<User:0x000000010ccea178 @email="ok23@ok.com", @id=2, @password="123">
181
+ irb(main):012> user.email = "new@ok.com"
182
+ => "new@ok.com"
183
+ irb(main):013> user
184
+ => #<User:0x000000010ccea178 @email="new@ok.com", @id=2, @password="123">
185
+ irb(main):014> user.reload
186
+ => #<User:0x000000010c488548 @email="ok23@ok.com", @id=2, @password="123"> # not persited data was updated from db
187
+ ```
188
+
189
+ Assign attributes without persisiting it to db
190
+ ```Ruby
191
+ irb(main):008> User.last.assign_attributes(email: "bb@ok.com")
192
+ => {"id" => 2, "email" => "ok23@ok.com", "password" => "123"
193
+ ```
194
+
195
+ Get all records scoped by field
196
+ ```Ruby
197
+ irb(main):005> User.where(email: "ok23@ok.com")
198
+ => [#<User:0x000000010cfaa5c0 @email="ok23@ok.com", @id=2, @password="123">]
199
+ ```
200
+
201
+ Get all record
202
+ ```Ruby
203
+ irb(main):001> User.all
204
+ => [#<User:0x000000010c239a30 @email="ok@ok.com", @id=1, @password="password">]
205
+ ```
206
+ Find by id
207
+ ```Ruby
208
+ irb(main):002> user = User.find 1
209
+ => #<User:0x000000010c2f7cd8 @email="ok@ok.com", @id=1, @password="password">
210
+ ```
211
+
212
+ Get last record
213
+ ```Ruby
214
+ irb(main):003> User.last
215
+ => #<User:0x000000010c2f7cd8 @email="ok@ok.com", @id=1, @password="password">
216
+ ```
217
+
218
+ Create new persited record
219
+ ```Ruby
220
+ irb(main):004> User.create(email: "ok23@ok.com", password: 123)
221
+ => #<User:0x000000010c393818 @email="ok23@ok.com", @id=2, @password=123>
222
+ ```
223
+
224
+ Destroy record and all related records
225
+ ```Ruby
226
+ irb(main):021> user.destroy(cascade: true)
227
+ => 1
228
+ ```
229
+
230
+ Destroy all records one by one
231
+ ```Ruby
232
+ irb(main):022> User.destroy_all
233
+ => [#<User:0x000000010d42df98 @email="ok@ok.com", @id=1, @password="password">, #<User:0x000000010d42de80 @email="ok23@ok.com", @id=2, @password="123">
234
+ irb(main):023> User.all
235
+ => []
236
+ ```
237
+
238
+ Use complex queries chains and when ready serialize it back to Rubee object.
239
+ ```Ruby
240
+ # user model
241
+ class User < Rubee::SequelObject
242
+ attr_accessor :id, :email, :password
243
+ owns_many :comments, over: :posts
244
+ end
245
+
246
+ # comment model
247
+ class Comment < Rubee::SequelObject
248
+ attr_accessor :id, :text, :user_id
249
+ owns_many :users, over: :posts
250
+ end
251
+
252
+ # join post model
253
+ class Post < Rubee::SequelObject
254
+ attr_accessor :id, :user_id, :comment_id
255
+ holds :comment
256
+ holds :user
257
+ end
258
+ ```
259
+
260
+ ```Ruby
261
+ irb(main):001> comment = Comment.new(text: "test")
262
+ irb(main):002> comment.save
263
+ irb(main):003> user = User.new(email: "ok-test@test.com", password: "123")
264
+ irb(main):004> user.save
265
+ irb(main):005> post = Post.new(user_id: user.id, comment_id: comment.id)
266
+ irb(main):006> post.save
267
+ => true
268
+ irb(main):007> comment
269
+ => #<Comment:0x000000012281a650 @id=21, @text="test">
270
+ irb(main):008> result = Comment.dataset.join(:posts, comment_id: :id)
271
+ irb(main):009> .where(comment_id: Comment.where(text: "test").last.id)
272
+ irb(main):010> .then { |dataset| Comment.serialize(dataset) }
273
+ => [#<Comment:0x0000000121889998 @id=30, @text="test", @user_id=702>]
274
+ ```
275
+ This is recommended when you want to run one query and serialize it back to Rubee object only once.
276
+ So it may safe some resources.
277
+
278
+
279
+
148
280
  ## Views
149
- View in ru.Bee is just a plain html/erb file that can be rendered from the controller.
281
+ View in ruBee is just a plain html/erb file that can be rendered from the controller.
150
282
 
151
283
  ## Object hooks
152
284
 
153
- In ru.Bee by extending Hookable module any Ruby object can be charged with hooks (logic),
285
+ In ruBee by extending Hookable module any Ruby object can be charged with hooks (logic),
154
286
  that can be executed before, after and around a specific method execution.
155
287
 
156
288
  Here below a controller example. However it can be used in any Ruby object, like Model etc.
@@ -268,7 +400,7 @@ rubee console # start the console
268
400
  rubee test # run all tests
269
401
  rubee test auth_tokenable_test.rb # run specific tests
270
402
  ```
271
- If you want to run any ru.Bee command within a specific ENV make sure you added it before a command.
403
+ If you want to run any ruBee command within a specific ENV make sure you added it before a command.
272
404
  For instance if you want to run console in test environment you need to run the following command
273
405
 
274
406
  ```bash
@@ -349,8 +481,32 @@ end
349
481
  TestAsyncRunnner.new.perform_async(options: {"email"=> "new@new.com", "password"=> "123"})
350
482
  ```
351
483
 
352
- ## TODOs
353
- - [x] Token authorization API
354
- - [ ] Document authorization API
355
- - [ ] Add test coverage
356
- - [ ] Fix bugs
484
+ ### Contributing
485
+
486
+ You are more than welcome to contribute to ruBee! To do so, please follow these steps:
487
+
488
+ 1. Fork the repository by clicking the "Fork" button on the GitHub page.
489
+
490
+ 2. Clone your fork:
491
+ ```bash
492
+ git clone https://github.com/your-username/rubee.git
493
+ ```
494
+
495
+ 3. Create a new branch for your feature or bug fix:
496
+ ```bash
497
+ git checkout -b feature/your-feature-name
498
+ ```
499
+
500
+ 4. Make your changes and commit them with descriptive messages:
501
+ ```bash
502
+ git commit -m "Add feature: [brief description of feature]"
503
+ ```
504
+
505
+ 5. Push your changes to your fork:
506
+ ```bash
507
+ git push origin feature/your-feature-name
508
+ ```
509
+
510
+ 6. Submit a pull request to the main branch of the original repository.
511
+
512
+ Let's make it shine even brighter!
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ru.Bee
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.32
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Saltykov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-22 00:00:00.000000000 Z
10
+ date: 2025-04-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -43,13 +43,17 @@ files:
43
43
  - lib/Dockerfile
44
44
  - lib/app/controllers/welcome_controller.rb
45
45
  - lib/app/models/user.rb
46
+ - lib/app/views/layout.erb
47
+ - lib/app/views/welcome_header.erb
46
48
  - lib/app/views/welcome_show.erb
47
49
  - lib/config.ru
48
50
  - lib/config/base_configuration.rb
49
51
  - lib/config/routes.rb
52
+ - lib/db/create_accounts.rb
53
+ - lib/db/create_comments.rb
54
+ - lib/db/create_posts.rb
50
55
  - lib/db/create_users.rb
51
56
  - lib/db/structure.rb
52
- - lib/db/test.db
53
57
  - lib/images/rubee.svg
54
58
  - lib/inits/print_colors.rb
55
59
  - lib/rubee.rb
@@ -63,10 +67,17 @@ files:
63
67
  - lib/rubee/controllers/middlewares/auth_token_middleware.rb
64
68
  - lib/rubee/extensions/hookable.rb
65
69
  - lib/rubee/extensions/serializable.rb
66
- - lib/rubee/models/database_object.rb
70
+ - lib/rubee/models/database_objectable.rb
67
71
  - lib/rubee/models/sequel_object.rb
72
+ - lib/tests/account_model_test.rb
68
73
  - lib/tests/auth_tokenable_test.rb
74
+ - lib/tests/comment_model_test.rb
75
+ - lib/tests/example_models/account.rb
76
+ - lib/tests/example_models/comment.rb
77
+ - lib/tests/example_models/post.rb
78
+ - lib/tests/example_models/user.rb
69
79
  - lib/tests/rubeeapp_test.rb
80
+ - lib/tests/test.db
70
81
  - lib/tests/test_helper.rb
71
82
  - lib/tests/user_model_test.rb
72
83
  - readme.md
@@ -1,50 +0,0 @@
1
- module Rubee
2
- class DatabaseObject
3
- include Serializable
4
- include Hookable
5
-
6
- def destroy
7
- end
8
-
9
- def save
10
- end
11
-
12
- def update(args = {})
13
- end
14
-
15
- def reload
16
- end
17
-
18
- class << self
19
- def last
20
- end
21
-
22
- def connection
23
- end
24
-
25
- def all
26
- end
27
-
28
- def find(id)
29
- end
30
-
31
- def where(args)
32
- end
33
-
34
- def create(attrs)
35
- end
36
-
37
- def pluralize_class_name
38
- word = self.name.downcase
39
- # Basic pluralization rules
40
- if word.end_with?('y') && !%w[a e i o u].include?(word[-2])
41
- word[0..-2] + 'ies' # Replace "y" with "ies"
42
- elsif word.end_with?('s', 'x', 'z', 'ch', 'sh')
43
- word + 'es' # Add "es" for certain endings
44
- else
45
- word + 's' # Default to adding "s"
46
- end
47
- end
48
- end
49
- end
50
- end