roda-project 0.1.8 → 0.1.10

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: 6e2e4f0e1a643cc30ca1efff43c571e29acf2ef47385c8fb6b5b1b1553641d6b
4
- data.tar.gz: 0f81e9706caee934a023ba77243fc21447a39279719713e610c237136ae66a01
3
+ metadata.gz: b31e88696427968c61a574f01c1860abcede888273244d75d258bacc35da4935
4
+ data.tar.gz: b24dc4b5f0ad6a8e9acc24dd192e927c6c6251dd3d163088daf862e941d52e71
5
5
  SHA512:
6
- metadata.gz: 4f27ffba5ba456ec2d937f69f1781f58193fc27567bf0ac3d7ea83edad23e948f3d127b3f608ca354cec3746bad1dcba85203bcd529a64c156734a38ded7a9d2
7
- data.tar.gz: 104494e4ff9bb346b440ac9b0be67d430653861079cdea90e99e813a7aad8236d63609bfbd0fb8bf2bb3043718e3fb6e8001cd5ccae59ee384a22eb4ba9123b8
6
+ metadata.gz: 0fdf4cc809987b8b6167e06499989a9d101f39923aba2d7493fcc479609d141b9dd24229f33bae3fd897604ab92f812179ae7c13be7148579d1ba822b3691c14
7
+ data.tar.gz: 6206ac4f835a181f839277576bb52b572b25a2b163e2a9403819c8b0d4ff5a0ce913871083328a9c01f4c8f146938fa42f733f7a94a5f7130f448689675f8b75
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Roda
4
+ module Project
5
+ module Bin
6
+ class Generators < ::Thor
7
+ class Migration
8
+ class ActionDetector
9
+ include Roda::Project::Helpers::Inflections
10
+
11
+ attr_reader :name, :args
12
+
13
+ def initialize(name, args: [])
14
+ @name = name.to_s
15
+ @args = args || []
16
+ end
17
+
18
+ def detect
19
+ camel_name = camelize(name)
20
+
21
+ if (m = camel_name.match(/^CreateJoinTable(?<t1>[A-Z][a-zA-Z0-9]*?)(?<t2>[A-Z][a-zA-Z0-9]*)$/)) ||
22
+ (m = camel_name.match(/JoinTable(?<t1>[A-Z][a-zA-Z0-9]*?)(?<t2>[A-Z][a-zA-Z0-9]*)$/)) ||
23
+ camel_name.start_with?("CreateJoinTable") || camel_name.include?("JoinTable")
24
+ tables = parse_join_tables(m)
25
+ { action: :create_join_table, tables: tables }
26
+ elsif (m = camel_name.match(/^Create(?<table_name>[A-Z0-9].*)$/))
27
+ { action: :create_table, table_name: pluralize(underscore(m[:table_name])) }
28
+ elsif (m = camel_name.match(/^Add.+To(?<table_name>[A-Z0-9].*)$/))
29
+ { action: :add_columns, table_name: pluralize(underscore(m[:table_name])) }
30
+ elsif (m = camel_name.match(/^Remove.+From(?<table_name>[A-Z0-9].*)$/))
31
+ { action: :drop_columns, table_name: pluralize(underscore(m[:table_name])) }
32
+ else
33
+ { action: :generic, table_name: nil }
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def parse_join_tables(match)
40
+ if args.size >= 2
41
+ args[0..1].map do |arg|
42
+ clean = underscore(arg.to_s.gsub(/:.*$/, ""))
43
+ s_clean = singularize(clean)
44
+ p_clean = pluralize(s_clean)
45
+ ["#{s_clean}_id", p_clean]
46
+ end
47
+ elsif match
48
+ raw1 = underscore(match[:t1])
49
+ raw2 = underscore(match[:t2])
50
+ s1 = singularize(raw1)
51
+ s2 = singularize(raw2)
52
+ t1 = pluralize(s1)
53
+ t2 = pluralize(s2)
54
+ [["#{s1}_id", t1], ["#{s2}_id", t2]]
55
+ else
56
+ []
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Roda
4
+ module Project
5
+ module Bin
6
+ class Generators
7
+ class Migration
8
+ class CodeBuilder
9
+ attr_reader :detection, :fields
10
+
11
+ def initialize(detection:, fields:)
12
+ @detection = detection || {}
13
+ @fields = fields || []
14
+ end
15
+
16
+ def build
17
+ action = detection[:action]
18
+
19
+ case action
20
+ when :create_table
21
+ build_create_table
22
+ when :add_columns
23
+ build_add_columns
24
+ when :drop_columns
25
+ build_drop_columns
26
+ when :create_join_table
27
+ build_create_join_table
28
+ else
29
+ build_generic
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_create_table
36
+ table = detection[:table_name]
37
+ lines = []
38
+ lines << "Sequel.migration do"
39
+ lines << " change do"
40
+ lines << " create_table(:#{table}) do"
41
+ lines << " primary_key :id"
42
+
43
+ indexes = []
44
+
45
+ fields.each do |field|
46
+ if field[:is_reference]
47
+ ref_line = " foreign_key :#{field[:name]}, :#{field[:target_table]}"
48
+ ref_line += format_options(field[:options]) unless field[:options].empty?
49
+ lines << ref_line
50
+ indexes << " index :#{field[:name]}" if field[:index]
51
+ else
52
+ col_line = " #{field[:type]} :#{field[:name]}"
53
+ col_line += format_options(field[:options]) unless field[:options].empty?
54
+ lines << col_line
55
+
56
+ if field[:index] == :unique
57
+ indexes << " index :#{field[:name]}, unique: true"
58
+ elsif field[:index]
59
+ indexes << " index :#{field[:name]}"
60
+ end
61
+
62
+ if field[:composite_index]
63
+ cols_str = field[:composite_index].map { |c| ":#{c}" }.join(", ")
64
+ indexes << " index [#{cols_str}]"
65
+ end
66
+ end
67
+ end
68
+
69
+ lines << " DateTime :created_at"
70
+ lines << " DateTime :updated_at"
71
+
72
+ unless indexes.empty?
73
+ lines << ""
74
+ lines.concat(indexes)
75
+ end
76
+
77
+ lines << " end"
78
+ lines << " end"
79
+ lines << "end"
80
+ lines.join("\n") + "\n"
81
+ end
82
+
83
+ def build_add_columns
84
+ table = detection[:table_name]
85
+ lines = []
86
+ lines << "Sequel.migration do"
87
+ lines << " change do"
88
+ lines << " alter_table(:#{table}) do"
89
+
90
+ fields.each do |field|
91
+ if field[:is_reference]
92
+ ref_line = " add_foreign_key :#{field[:name]}, :#{field[:target_table]}"
93
+ ref_line += format_options(field[:options]) unless field[:options].empty?
94
+ lines << ref_line
95
+ lines << " add_index :#{field[:name]}" if field[:index]
96
+ else
97
+ col_line = " add_column :#{field[:name]}, #{field[:type]}"
98
+ col_line += format_options(field[:options]) unless field[:options].empty?
99
+ lines << col_line
100
+
101
+ if field[:index] == :unique
102
+ lines << " add_index :#{field[:name]}, unique: true"
103
+ elsif field[:index]
104
+ lines << " add_index :#{field[:name]}"
105
+ end
106
+
107
+ if field[:composite_index]
108
+ cols_str = field[:composite_index].map { |c| ":#{c}" }.join(", ")
109
+ lines << " add_index [#{cols_str}]"
110
+ end
111
+ end
112
+ end
113
+
114
+ lines << " end"
115
+ lines << " end"
116
+ lines << "end"
117
+ lines.join("\n") + "\n"
118
+ end
119
+
120
+ def build_drop_columns
121
+ table = detection[:table_name]
122
+ lines = []
123
+ lines << "Sequel.migration do"
124
+ lines << " change do"
125
+ lines << " alter_table(:#{table}) do"
126
+
127
+ fields.each do |field|
128
+ lines << " drop_column :#{field[:name]}"
129
+ end
130
+
131
+ lines << " end"
132
+ lines << " end"
133
+ lines << "end"
134
+ lines.join("\n") + "\n"
135
+ end
136
+
137
+ def build_create_join_table
138
+ tables = detection[:tables] || []
139
+ args_str = tables.map { |fk, tbl| "#{fk}: :#{tbl}" }.join(", ")
140
+
141
+ lines = []
142
+ lines << "Sequel.migration do"
143
+ lines << " change do"
144
+ lines << " create_join_table(#{args_str})"
145
+ lines << " end"
146
+ lines << "end"
147
+ lines.join("\n") + "\n"
148
+ end
149
+
150
+ def build_generic
151
+ <<~RUBY
152
+ Sequel.migration do
153
+ up do
154
+ # add your migration here
155
+ end
156
+
157
+ down do
158
+ # remove your migration here
159
+ end
160
+ end
161
+ RUBY
162
+ end
163
+
164
+ def format_options(opts)
165
+ return "" if opts.empty?
166
+
167
+ formatted = opts.map do |k, v|
168
+ val_str = case v
169
+ when Array
170
+ "[#{v.join(', ')}]"
171
+ when Symbol
172
+ ":#{v}"
173
+ else
174
+ v.inspect
175
+ end
176
+ "#{k}: #{val_str}"
177
+ end.join(", ")
178
+
179
+ ", #{formatted}"
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Roda
4
+ module Project
5
+ module Bin
6
+ class Generators
7
+ class Migration
8
+ class FieldParser
9
+ TYPE_MAP = {
10
+ "string" => "String",
11
+ "text" => "String",
12
+ "integer" => "Integer",
13
+ "bigint" => "Bignum",
14
+ "float" => "Float",
15
+ "decimal" => "BigDecimal",
16
+ "datetime" => "DateTime",
17
+ "timestamp" => "DateTime",
18
+ "time" => "Time",
19
+ "date" => "Date",
20
+ "boolean" => "TrueClass",
21
+ "binary" => "File",
22
+ "json" => ":json",
23
+ "jsonb" => ":jsonb",
24
+ "uuid" => ":uuid"
25
+ }.freeze
26
+
27
+ attr_reader :raw_fields
28
+
29
+ def initialize(raw_fields)
30
+ @raw_fields = raw_fields || []
31
+ end
32
+
33
+ def parse
34
+ fields = []
35
+
36
+ raw_fields.each do |raw_field|
37
+ parts = raw_field.to_s.split(":")
38
+ next if parts.empty?
39
+
40
+ name = parts[0]
41
+ type_part = parts[1] || "string"
42
+ index_part = parts[2]
43
+
44
+ # Extract modifier e.g. string{50}, decimal{10.2}, references{polymorphic}
45
+ type, modifier = extract_type_and_modifier(type_part)
46
+
47
+ if type == "references" || type == "belongs_to"
48
+ if modifier == "polymorphic"
49
+ fields << {
50
+ name: "#{name}_id",
51
+ type: "Bignum",
52
+ options: {},
53
+ index: false,
54
+ composite_index: ["#{name}_type", "#{name}_id"]
55
+ }
56
+ fields << {
57
+ name: "#{name}_type",
58
+ type: "String",
59
+ options: {},
60
+ index: false
61
+ }
62
+ else
63
+ singular_name = singularize(name)
64
+ plural_table = pluralize(singular_name)
65
+ fields << {
66
+ name: "#{name}_id",
67
+ is_reference: true,
68
+ target_table: plural_table,
69
+ index: true,
70
+ options: {}
71
+ }
72
+ end
73
+ else
74
+ sequel_type = TYPE_MAP[type] || "String"
75
+ options = {}
76
+
77
+ options[:text] = true if type == "text"
78
+
79
+ if modifier
80
+ if modifier.include?(".")
81
+ prec, scale = modifier.split(".").map(&:to_i)
82
+ options[:size] = [prec, scale]
83
+ elsif modifier =~ /^\d+$/
84
+ options[:size] = modifier.to_i
85
+ end
86
+ end
87
+
88
+ index_val = parse_index_spec(index_part)
89
+
90
+ fields << {
91
+ name: name,
92
+ type: sequel_type,
93
+ options: options,
94
+ index: index_val
95
+ }
96
+ end
97
+ end
98
+
99
+ fields
100
+ end
101
+
102
+ private
103
+
104
+ def extract_type_and_modifier(type_part)
105
+ if type_part =~ /^(.*?)\{(.*?)\}$/
106
+ [Regexp.last_match(1), Regexp.last_match(2)]
107
+ else
108
+ [type_part, nil]
109
+ end
110
+ end
111
+
112
+ def parse_index_spec(index_part)
113
+ case index_part
114
+ when "uniq", "unique"
115
+ :unique
116
+ when "index"
117
+ true
118
+ else
119
+ false
120
+ end
121
+ end
122
+
123
+ def singularize(str)
124
+ if str.end_with?("ies")
125
+ "#{str[0..-4]}y"
126
+ elsif str.end_with?("es") && !str.end_with?("ques")
127
+ str[0..-3]
128
+ elsif str.end_with?("s") && !str.end_with?("ss")
129
+ str[0..-2]
130
+ else
131
+ str
132
+ end
133
+ end
134
+
135
+ def pluralize(str)
136
+ return str if str.end_with?("s")
137
+
138
+ if str.end_with?("y") && !str.end_with?("ay", "ey", "oy", "uy")
139
+ "#{str[0..-2]}ies"
140
+ elsif str.end_with?("s", "x", "z", "ch", "sh")
141
+ "#{str}es"
142
+ else
143
+ "#{str}s"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,4 +1,5 @@
1
- # rubocop:disable Layout/HeredocIndentation
1
+ # frozen_string_literal: true
2
+
2
3
  class Roda
3
4
  module Project
4
5
  module Bin
@@ -18,19 +19,12 @@ class Roda
18
19
  end.max || 0
19
20
  next_number = (max_number + 1).to_s.rjust(3, "0")
20
21
 
21
- filename = File.join(migrations_path, "#{next_number}_#{migration_name}.rb")
22
-
23
- content = <<~RUBY
24
- Sequel.migration do
25
- up do
26
- # add your migration here
27
- end
22
+ formatted_name = underscore(migration_name)
23
+ filename = File.join(migrations_path, "#{next_number}_#{formatted_name}.rb")
28
24
 
29
- down do
30
- # remove your migration here
31
- end
32
- end
33
- RUBY
25
+ detection = ActionDetector.new(migration_name, args: field_args).detect
26
+ fields = FieldParser.new(field_args).parse
27
+ content = CodeBuilder.new(detection: detection, fields: fields).build
34
28
 
35
29
  File.write(filename, content)
36
30
  puts "* created migration: #{filename}"
@@ -43,9 +37,21 @@ class Roda
43
37
  def migration_name
44
38
  @migration_name ||= @args[0]
45
39
  end
40
+
41
+ def field_args
42
+ @field_args ||= (@args[1..] || [])
43
+ end
44
+
45
+ private
46
+
47
+ def underscore(str)
48
+ str.to_s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
49
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
50
+ .tr("-", "_")
51
+ .downcase
52
+ end
46
53
  end
47
54
  end
48
55
  end
49
56
  end
50
57
  end
51
- # rubocop:enable Layout/HeredocIndentation
@@ -13,6 +13,7 @@ class Roda
13
13
  generate_routes
14
14
  generate_views
15
15
  generate_tests
16
+ print_nested_branch_reminder
16
17
  end
17
18
 
18
19
  private
@@ -25,9 +26,19 @@ class Roda
25
26
  " r.#{method} \"#{name}\" do#{view_line}\n end"
26
27
  end.join("\n\n")
27
28
 
29
+ hash_branch_header = if branch_name.include?("/")
30
+ parts = branch_name.split("/")
31
+ sub_path = parts[0..-2].join("/")
32
+ namespace = sub_path.include?("/") ? ":\"#{sub_path}\"" : ":#{sub_path}"
33
+ branch_segment = parts.last
34
+ "hash_branch #{namespace}, \"#{branch_segment}\" do |r|"
35
+ else
36
+ "hash_branch \"#{branch_name}\" do |r|"
37
+ end
38
+
28
39
  content = <<~RUBY
29
40
  class #{@context.const_project_name}
30
- hash_branch "#{branch_name}" do |r|
41
+ #{hash_branch_header}
31
42
  #{route_definitions}
32
43
  end
33
44
  end
@@ -99,6 +110,20 @@ class Roda
99
110
  def branch_name
100
111
  @branch_name ||= @args[0]
101
112
  end
113
+
114
+ def print_nested_branch_reminder
115
+ return unless branch_name.include?("/")
116
+
117
+ parts = branch_name.split("/")
118
+ sub_path = parts[0..-2].join("/")
119
+ namespace = sub_path.include?("/") ? ":\"#{sub_path}\"" : ":#{sub_path}"
120
+
121
+ puts "\ndont forget to add:\n\n" \
122
+ "autoload_hash_branch_dir(#{namespace}, \"./app/routes/#{sub_path}\")\n\n" \
123
+ "route do |r|\n" \
124
+ " r.on(\"#{sub_path}\") { r.hash_branches(#{namespace}) }\n" \
125
+ "end\n"
126
+ end
102
127
  end
103
128
  end
104
129
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Roda
4
+ module Project
5
+ module Helpers
6
+ module Inflections
7
+ module_function
8
+
9
+ def camelize(str)
10
+ return str if str =~ /[A-Z]/
11
+
12
+ str.split("_").map(&:capitalize).join
13
+ end
14
+
15
+ def underscore(str)
16
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
18
+ .tr("-", "_")
19
+ .downcase
20
+ end
21
+
22
+ def pluralize(str)
23
+ return str if str.end_with?("s")
24
+
25
+ if str.end_with?("y") && !str.end_with?("ay", "ey", "oy", "uy")
26
+ "#{str[0..-2]}ies"
27
+ elsif str.end_with?("s", "x", "z", "ch", "sh")
28
+ "#{str}es"
29
+ else
30
+ "#{str}s"
31
+ end
32
+ end
33
+
34
+ def singularize(str)
35
+ if str.end_with?("ies")
36
+ "#{str[0..-4]}y"
37
+ elsif str.end_with?("es") && !str.end_with?("ques")
38
+ str[0..-3]
39
+ elsif str.end_with?("s") && !str.end_with?("ss")
40
+ str[0..-2]
41
+ else
42
+ str
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,6 +1,6 @@
1
1
  class <%= context.const_project_name %> < Roda
2
2
  # Routing<% if context.fullstack? %>
3
- plugin :hash_branch_view_subdir<% end %>
3
+ plugin :hash_branch_view_namespace<% end %>
4
4
  plugin :autoload_hash_branches
5
5
  autoload_hash_branch_dir("./app/routes")
6
6
  plugin :all_verbs
@@ -23,6 +23,7 @@ gem "puma"
23
23
 
24
24
  # Views
25
25
  gem 'roda-assets_manifest'
26
+ gem 'roda-hash_branch_view_namespace'
26
27
  <% end %>
27
28
  # Performance
28
29
  gem "oj"
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Roda
4
4
  module Project
5
- VERSION = "0.1.8"
5
+ VERSION = "0.1.10"
6
6
  end
7
7
  end
data/lib/roda/project.rb CHANGED
@@ -55,14 +55,21 @@ require "thor"
55
55
  require_relative "project/helpers/ids"
56
56
  require_relative "project/helpers/interactive_input"
57
57
  require_relative "project/helpers/template"
58
+ require_relative "project/helpers/inflections"
58
59
  # Base
59
60
  require_relative "project/main_context"
60
61
  require_relative "project/generator"
61
62
  require_relative "project/cli"
62
- # Generators
63
+ # Generators/base
63
64
  require_relative "project/bin/generator"
65
+ # Generators/migration
64
66
  require_relative "project/bin/generators/migration"
67
+ require_relative "project/bin/generators/migration/action_detector"
68
+ require_relative "project/bin/generators/migration/field_parser"
69
+ require_relative "project/bin/generators/migration/code_builder"
70
+ # Generators/routes
65
71
  require_relative "project/bin/generators/routes"
72
+ # Generators/base
66
73
  require_relative "project/bin/generators"
67
74
  # Version
68
75
  require_relative "project/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda-project
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henrique F. Teixeira
@@ -80,10 +80,14 @@ files:
80
80
  - lib/roda/project/bin/generator.rb
81
81
  - lib/roda/project/bin/generators.rb
82
82
  - lib/roda/project/bin/generators/migration.rb
83
+ - lib/roda/project/bin/generators/migration/action_detector.rb
84
+ - lib/roda/project/bin/generators/migration/code_builder.rb
85
+ - lib/roda/project/bin/generators/migration/field_parser.rb
83
86
  - lib/roda/project/bin/generators/routes.rb
84
87
  - lib/roda/project/cli.rb
85
88
  - lib/roda/project/generator.rb
86
89
  - lib/roda/project/helpers/ids.rb
90
+ - lib/roda/project/helpers/inflections.rb
87
91
  - lib/roda/project/helpers/interactive_input.rb
88
92
  - lib/roda/project/helpers/template.rb
89
93
  - lib/roda/project/main_context.rb