PageRenderTime 45ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/generators/dry_scaffold/dry_scaffold_generator.rb

https://github.com/michel/dry_scaffold
Ruby | 415 lines | 319 code | 70 blank | 26 comment | 14 complexity | 76dc161e061478f82e88d684d26800bf MD5 | raw file
  1. require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'dry_generator'))
  2. require File.expand_path(File.join(File.dirname(__FILE__), '..', 'dry_model', 'dry_model_generator'))
  3. class DryScaffoldGenerator < DryGenerator
  4. # Banner: Generator arguments and options.
  5. BANNER_ARGS = [
  6. "[_actions:new,create,...]",
  7. "[_formats:html,json,...]",
  8. DryModelGenerator::BANNER_ARGS
  9. ].freeze
  10. BANNER_OPTIONS = [
  11. "[--skip-pagination]",
  12. "[--skip-search]",
  13. "[--skip-resourceful]",
  14. "[--skip-formtastic]",
  15. "[--skip-views]",
  16. "[--skip-builders]",
  17. "[--skip-helpers]",
  18. "[--layout]",
  19. DryModelGenerator::BANNER_OPTIONS
  20. ].freeze
  21. # Paths.
  22. CONTROLLERS_PATH = File.join('app', 'controllers').freeze
  23. HELPERS_PATH = File.join('app', 'helpers').freeze
  24. VIEWS_PATH = File.join('app', 'views').freeze
  25. LAYOUTS_PATH = File.join(VIEWS_PATH, 'layouts').freeze
  26. ROUTES_FILE_PATH = File.join(RAILS_ROOT, 'config', 'routes.rb').freeze
  27. # Formats.
  28. DEFAULT_RESPOND_TO_FORMATS = [:html, :xml, :json].freeze
  29. ENHANCED_RESPOND_TO_FORMATS = [:yml, :yaml, :txt, :text, :atom, :rss].freeze
  30. RESPOND_TO_FEED_FORMATS = [:atom, :rss].freeze
  31. # Actions.
  32. DEFAULT_MEMBER_ACTIONS = [:show, :new, :edit, :create, :update, :destroy].freeze
  33. DEFAULT_MEMBER_AUTOLOAD_ACTIONS = (DEFAULT_MEMBER_ACTIONS - [:new, :create])
  34. DEFAULT_COLLECTION_ACTIONS = [:index].freeze
  35. DEFAULT_COLLECTION_AUTOLOAD_ACTIONS = DEFAULT_COLLECTION_ACTIONS
  36. DEFAULT_CONTROLLER_ACTIONS = (DEFAULT_COLLECTION_ACTIONS + DEFAULT_MEMBER_ACTIONS)
  37. DEFAULT_VIEW_TEMPLATE_FORMAT = :haml
  38. RESOURCEFUL_COLLECTION_NAME = 'collection'.freeze
  39. RESOURCEFUL_SINGULAR_NAME = 'resource'.freeze
  40. # :{action} => [:{partial}, ...]
  41. ACTION_VIEW_TEMPLATES = {
  42. :index => [:item],
  43. :show => [],
  44. :new => [:form],
  45. :edit => [:form]
  46. }.freeze
  47. ACTION_FORMAT_BUILDERS = {
  48. :index => [:atom, :rss]
  49. }
  50. attr_reader :controller_name,
  51. :controller_class_path,
  52. :controller_file_path,
  53. :controller_class_nesting,
  54. :controller_class_nesting_depth,
  55. :controller_class_name,
  56. :controller_underscore_name,
  57. :controller_singular_name,
  58. :controller_plural_name,
  59. :collection_name,
  60. :model_singular_name,
  61. :model_plural_name,
  62. :view_template_format,
  63. :actions,
  64. :formats,
  65. :config
  66. alias_method :controller_file_name, :controller_underscore_name
  67. alias_method :controller_table_name, :controller_plural_name
  68. def initialize(runtime_args, runtime_options = {})
  69. super(runtime_args, runtime_options)
  70. @controller_name = @name.pluralize
  71. base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
  72. @controller_class_name_without_nesting, @controller_underscore_name, @controller_plural_name = inflect_names(base_name)
  73. @controller_singular_name = base_name.singularize
  74. if @controller_class_nesting.empty?
  75. @controller_class_name = @controller_class_name_without_nesting
  76. else
  77. @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
  78. end
  79. @view_template_format = options[:view_template_format] || DEFAULT_VIEW_TEMPLATE_FORMAT
  80. @attributes ||= []
  81. @args_for_model ||= []
  82. # Non-attribute args, i.e. "_actions:new,create".
  83. @args.each do |arg|
  84. arg_entities = arg.split(':')
  85. if arg =~ /^#{NON_ATTR_ARG_KEY_PREFIX}/
  86. if arg =~ /^#{NON_ATTR_ARG_KEY_PREFIX}action/
  87. # Replace quantifiers with default actions.
  88. arg_entities[1].gsub!(/\*/, DEFAULT_CONTROLLER_ACTIONS.join(','))
  89. arg_entities[1].gsub!(/new\+/, [:new, :create].join(','))
  90. arg_entities[1].gsub!(/edit\+/, [:edit, :update].join(','))
  91. arg_actions = arg_entities[1].split(',').compact.uniq
  92. @actions = arg_actions.collect { |action| action.downcase.to_sym }
  93. elsif arg =~ /^#{NON_ATTR_ARG_KEY_PREFIX}(format|respond_to)/
  94. # Replace quantifiers with default respond_to-formats.
  95. arg_entities[1].gsub!(/\*/, DEFAULT_RESPOND_TO_FORMATS.join(','))
  96. arg_formats = arg_entities[1].split(',').compact.uniq
  97. @formats = arg_formats.collect { |format| format.downcase.to_sym }
  98. elsif arg =~ /^#{NON_ATTR_ARG_KEY_PREFIX}index/
  99. @args_for_model << arg
  100. end
  101. else
  102. @attributes << Rails::Generator::GeneratedAttribute.new(*arg_entities)
  103. @args_for_model << arg
  104. end
  105. end
  106. @actions ||= DEFAULT_ARGS[:actions] || DEFAULT_CONTROLLER_ACTIONS
  107. @formats ||= DEFAULT_ARGS[:formats] || DEFAULT_RESPOND_TO_FORMATS
  108. @options = DEFAULT_OPTIONS.merge(options)
  109. end
  110. def manifest
  111. record do |m|
  112. # Check for class naming collisions.
  113. m.class_collisions "#{controller_class_name}Controller", "#{controller_class_name}ControllerTest"
  114. m.class_collisions "#{controller_class_name}Helper", "#{controller_class_name}HelperTest"
  115. # Controllers.
  116. controller_template = options[:resourceful] ? 'inherited_resources' : 'action'
  117. m.directory File.join(CONTROLLERS_PATH, controller_class_path)
  118. m.template File.join('controllers', "#{controller_template}_controller.rb"),
  119. File.join(CONTROLLERS_PATH, controller_class_path, "#{controller_file_name}_controller.rb")
  120. # Controller Tests.
  121. unless options[:skip_tests] || options[:skip_controller_tests]
  122. controller_tests_path = File.join(TEST_PATHS[test_framework], FUNCTIONAL_TESTS_PATH[test_framework])
  123. m.directory File.join(controller_tests_path, controller_class_path)
  124. m.template File.join('controllers', 'tests', "#{test_framework}", 'functional_test.rb'),
  125. File.join(controller_tests_path, controller_class_path, "#{controller_file_name}_controller_#{TEST_POST_FIX[test_framework]}.rb")
  126. end
  127. # Helpers.
  128. unless options[:skip_helpers]
  129. m.directory File.join(HELPERS_PATH, controller_class_path)
  130. m.template File.join('helpers', 'helper.rb'),
  131. File.join(HELPERS_PATH, controller_class_path, "#{controller_file_name}_helper.rb")
  132. # Helper Tests
  133. unless options[:skip_tests]
  134. helper_tests_path = File.join(TEST_PATHS[test_framework], 'helpers')
  135. m.directory File.join(helper_tests_path, controller_class_path)
  136. m.template File.join('helpers', 'tests', "#{test_framework}", 'unit_test.rb'),
  137. File.join(helper_tests_path, controller_class_path, "#{controller_file_name}_helper_#{TEST_POST_FIX[test_framework]}.rb")
  138. end
  139. end
  140. # Views.
  141. unless options[:skip_views]
  142. m.directory File.join(VIEWS_PATH, controller_class_path, controller_file_name)
  143. # View template for each action.
  144. (actions & ACTION_VIEW_TEMPLATES.keys).each do |action|
  145. m.template File.join('views', "#{view_template_format}", "#{action}.html.#{view_template_format}"),
  146. File.join(VIEWS_PATH, controller_file_name, "#{action}.html.#{view_template_format}")
  147. # View template for each partial - if not already copied.
  148. (ACTION_VIEW_TEMPLATES[action] || []).each do |partial|
  149. m.template File.join('views', "#{view_template_format}", "_#{partial}.html.#{view_template_format}"),
  150. File.join(VIEWS_PATH, controller_file_name, "_#{partial}.html.#{view_template_format}")
  151. end
  152. end
  153. end
  154. # Builders.
  155. unless options[:skip_builders]
  156. m.directory File.join(VIEWS_PATH, controller_class_path, controller_file_name)
  157. (actions & ACTION_FORMAT_BUILDERS.keys).each do |action|
  158. (formats & ACTION_FORMAT_BUILDERS[action] || []).each do |format|
  159. m.template File.join('views', 'builder', "#{action}.#{format}.builder"),
  160. File.join(VIEWS_PATH, controller_file_name, "#{action}.#{format}.builder")
  161. end
  162. end
  163. end
  164. # Layout.
  165. if options[:layout]
  166. m.directory File.join(LAYOUTS_PATH)
  167. m.template File.join('views', "#{view_template_format}", "layout.html.#{view_template_format}"),
  168. File.join(LAYOUTS_PATH, "#{controller_file_name}.html.#{view_template_format}")
  169. end
  170. # Routes.
  171. unless resource_route_exists?
  172. # TODO: Override Rails default method to not generate route if it's already defined.
  173. m.route_resources controller_file_name
  174. end
  175. # Models - use Rails default generator.
  176. m.dependency 'dry_model', [name] + @args_for_model, options.merge(:collision => :skip)
  177. end
  178. end
  179. ### Fixture/Factory Helpers.
  180. def build_object
  181. case options[:factory_framework]
  182. when :factory_girl then
  183. "Factory(:#{singular_name})"
  184. when :machinist then
  185. "#{class_name}.make"
  186. when :object_daddy then
  187. "#{class_name}.generate"
  188. else #:fixtures
  189. "#{table_name}(:basic)"
  190. end
  191. end
  192. ### Link Helpers.
  193. def collection_instance
  194. "@#{collection_name}"
  195. end
  196. def resource_instance
  197. "@#{singular_name}"
  198. end
  199. def index_path
  200. "#{collection_name}_path"
  201. end
  202. def new_path
  203. "new_#{singular_name}_path"
  204. end
  205. def show_path(object_name = resource_instance)
  206. "#{singular_name}_path(#{object_name})"
  207. end
  208. def edit_path(object_name = resource_instance)
  209. "edit_#{show_path(object_name)}"
  210. end
  211. def destroy_path(object_name = resource_instance)
  212. "#{object_name}"
  213. end
  214. def index_url
  215. "#{collection_name}_url"
  216. end
  217. def new_url
  218. "new_#{singular_name}_url"
  219. end
  220. def show_url(object_name = resource_instance)
  221. "#{singular_name}_url(#{object_name})"
  222. end
  223. def edit_url(object_name = resource_instance)
  224. "edit_#{show_url(object_name)}"
  225. end
  226. def destroy_url(object_name = resource_instance)
  227. "#{object_name}"
  228. end
  229. ### Feed Helpers.
  230. def feed_link(format)
  231. case format
  232. when :atom then
  233. ":href => #{plural_name}_url(:#{format}), :rel => 'self'"
  234. when :rss then
  235. "#{plural_name}_url(#{singular_name}, :#{format})"
  236. end
  237. end
  238. def feed_entry_link(format)
  239. case format
  240. when :atom then
  241. ":href => #{singular_name}_url(#{singular_name}, :#{format})"
  242. when :rss then
  243. "#{singular_name}_url(#{singular_name}, :#{format})"
  244. end
  245. end
  246. def feed_date(format)
  247. case format
  248. when :atom then
  249. "(#{collection_instance}.first.created_at rescue Time.now.utc).strftime('%Y-%m-%dT%H:%M:%SZ')"
  250. when :rss then
  251. "(#{collection_instance}.first.created_at rescue Time.now.utc).to_s(:rfc822)"
  252. end
  253. end
  254. def feed_entry_date(format)
  255. case format
  256. when :atom then
  257. "#{singular_name}.try(:updated_at).strftime('%Y-%m-%dT%H:%M:%SZ')"
  258. when :rss then
  259. "#{singular_name}.try(:updated_at).to_s(:rfc822)"
  260. end
  261. end
  262. protected
  263. def resource_route_exists?
  264. route_exp = "map.resources :#{controller_file_name}"
  265. File.read(ROUTES_FILE_PATH) =~ /(#{route_exp.strip}|#{route_exp.strip.tr('\'', '\"')})/
  266. end
  267. def assign_names!(name)
  268. super(name)
  269. @model_singular_name = @singular_name
  270. @model_plural_name = @plural_name
  271. @collection_name = options[:resourceful] ? RESOURCEFUL_COLLECTION_NAME : @model_plural_name
  272. @singular_name = options[:resourceful] ? RESOURCEFUL_SINGULAR_NAME : @model_singular_name
  273. @plural_name = options[:resourceful] ? RESOURCEFUL_SINGULAR_NAME.pluralize : @model_plural_name
  274. end
  275. def add_options!(opt)
  276. super(opt)
  277. ### CONTROLLER + VIEW + HELPER
  278. opt.separator ' '
  279. opt.separator 'Scaffold Options:'
  280. opt.on('--skip-resourceful',
  281. "Controller: Skip 'inherited_resources' style controllers and views. Requires gem 'josevalim-inherited_resources'.") do |v|
  282. options[:resourceful] = !v
  283. end
  284. opt.on('--skip-pagination',
  285. "Controller/View: Skip 'will_paginate' for collections in controllers and views. Requires gem 'mislav-will_paginate'.") do |v|
  286. options[:pagination] = !v
  287. end
  288. opt.on('--skip-search',
  289. "Controller/View: Skip 'searchlogic' for collections in controllers and views. Requires gem 'searchlogic'.") do |v|
  290. options[:search] = !v
  291. end
  292. opt.on('--skip-formtastic',
  293. "View: Skip 'formtastic' style forms. Requires gem 'justinfrench-formtastic'.") do |v|
  294. options[:formtastic] = !v
  295. end
  296. opt.on("--skip-controller-tests", "Controller: Skip generation of tests for controller.") do |v|
  297. options[:skip_controller_tests] = v
  298. end
  299. opt.on('--skip-views', "View: Skip generation of views.") do |v|
  300. options[:skip_views] = v
  301. end
  302. opt.on('--skip-builders', "View: Skip generation of builders.") do |v|
  303. options[:skip_builders] = v
  304. end
  305. opt.on('--layout', "View: Generate layout.") do |v|
  306. options[:layout] = v
  307. end
  308. opt.on('--skip-helper', "Helper: Skip generation of helpers.") do |v|
  309. options[:skip_helpers] = v
  310. end
  311. ### MODEL
  312. opt.separator ' '
  313. opt.separator 'Model Options:'
  314. opt.on("--skip-timestamps", "Model: Don't add timestamps to the migration file.") do |v|
  315. options[:skip_timestamps] = v
  316. end
  317. opt.on("--skip-migration", "Model: Skip generation of migration file.") do |v|
  318. options[:skip_migration] = v
  319. end
  320. opt.on("--skip-tests", "Model: Skip generation of tests.") do |v|
  321. options[:skip_tests] = v
  322. end
  323. opt.on("--skip-controller tests", "Controller: Skip generation of tests for controller.") do |v|
  324. options[:skip_controller_tests] = v
  325. end
  326. opt.separator ' '
  327. end
  328. def banner_args
  329. [BANNER_ARGS, super].flatten.join(' ')
  330. end
  331. def banner_options
  332. [BANNER_OPTIONS, super].flatten.join(' ')
  333. end
  334. def banner
  335. [super, banner_args, banner_options].join(' ')
  336. end
  337. end