diff --git a/spec/amber/cli/commands/generator_spec.cr b/spec/amber/cli/commands/generator_spec.cr index 969bf6cf8..62a1be5a8 100644 --- a/spec/amber/cli/commands/generator_spec.cr +++ b/spec/amber/cli/commands/generator_spec.cr @@ -196,6 +196,21 @@ module Amber::CLI end end end + + describe "sam" do + MainCommand.run %w(generate sam) + + it "generates sam file" do + File.exists?("./sam.cr").should be_true + end + + it "generates proper content" do + file_content = File.read("./sam.cr") + file_content.should contain("Sam.help") + file_content.should contain(%(require "sam")) + file_content.should contain(%(require "./config/*")) + end + end cleanup end end diff --git a/spec/amber/cli/commands/init_spec.cr b/spec/amber/cli/commands/init_spec.cr index 6d669797e..43afc6805 100644 --- a/spec/amber/cli/commands/init_spec.cr +++ b/spec/amber/cli/commands/init_spec.cr @@ -68,6 +68,15 @@ module Amber::CLI end end end + + context "--sam" do + cleanup + scaffold_app(TESTING_APP, "--sam") + + it "generates sam file" do + File.exists?("./sam.cr").should be_true + end + end end context "Database settings" do diff --git a/spec/amber/cli/templates/app_spec.cr b/spec/amber/cli/templates/app_spec.cr index ab5940114..8fd87ad96 100644 --- a/spec/amber/cli/templates/app_spec.cr +++ b/spec/amber/cli/templates/app_spec.cr @@ -2,19 +2,17 @@ require "../../../spec_helper" module Amber::CLI describe App do - pg_app = App.new("sample-app") - mysql_app = App.new("sample-app", "mysql") - sqlite_app = App.new("sample-app", "sqlite") + pg_app = App.new("sample-app", "pg", "slang", "jennifer", false) + mysql_app = App.new("sample-app", "mysql", "slang", "jennifer", false) + sqlite_app = App.new("sample-app", "sqlite", "slang", "jennifer", false) describe "#database_name_base" do it "should return a postgres compatible name" do pg_app.database_name_base.should_not contain "-" end - it "should return a consistent name for all db types" do - mysql_app.database_name_base.should eq pg_app.database_name_base - sqlite_app.database_name_base.should eq pg_app.database_name_base - end + it { mysql_app.database_name_base.should eq pg_app.database_name_base } + it { sqlite_app.database_name_base.should eq pg_app.database_name_base } end end end diff --git a/spec/amber/cli/templates/jennifer_migration_spec.cr b/spec/amber/cli/templates/jennifer_migration_spec.cr new file mode 100644 index 000000000..f9540ace6 --- /dev/null +++ b/spec/amber/cli/templates/jennifer_migration_spec.cr @@ -0,0 +1,182 @@ +require "../../../spec_helper" +require "./migration_spec_helper" + +module Amber::CLI::Jennifer + describe Migration do + described_class = Amber::CLI::Jennifer::Migration + + describe ".build" do + context "with empty fields" do + it "builds unspecified migration for non new table migration name" do + described_class.build("add_field_to_tables", %w()).is_a?(Amber::CLI::Jennifer::Migration).should be_true + described_class.build("remove_field_from_tables", %w()).is_a?(Amber::CLI::Jennifer::Migration).should be_true + described_class.build("some_gibberish", %w()).is_a?(Amber::CLI::Jennifer::Migration).should be_true + end + + it "builds new table migration for new migration name pattern" do + described_class.build("create_tables", %w()).is_a?(Amber::CLI::Jennifer::CreateTableMigration).should be_true + end + end + + context "with new table migration name pattern" do + it do + described_class.build("create_tables", %w(name:string)) + .is_a?(Amber::CLI::Jennifer::CreateTableMigration).should be_true + end + + it "correctly sets table name" do + described_class.build("create_tables", %w(field:string)) + .as(Amber::CLI::Jennifer::CreateTableMigration) + .table_name.should eq("tables") + end + end + + context "with add new column migration name pattern" do + it do + migration = described_class.build("add_field_to_tables", %w(field:string)) + migration.is_a?(Amber::CLI::Jennifer::ChangeColumnsMigration).should be_true + migration.as(Amber::CLI::Jennifer::ChangeColumnsMigration).add?.should be_true + end + + it "correctly sets table name" do + described_class.build("add_field_to_tables", %w(field:string)) + .as(Amber::CLI::Jennifer::ChangeColumnsMigration) + .table_name.should eq("tables") + end + end + + context "with remove column migration name pattern" do + it do + migration = described_class.build("remove_field_from_tables", %w(field:string)) + migration.is_a?(Amber::CLI::Jennifer::ChangeColumnsMigration).should be_true + migration.as(Amber::CLI::Jennifer::ChangeColumnsMigration).remove?.should be_true + end + + it "correctly sets table name" do + described_class.build("add_field_to_tables", %w(field:string)) + .as(Amber::CLI::Jennifer::ChangeColumnsMigration) + .table_name.should eq("tables") + end + end + end + + describe "#render" do + migration = described_class.new("some_migration", %w(field:string)) + migration_file = MigrationSpecHelper.text_for(migration) + + it do + migration_file.should contain "class SomeMigration < Jennifer::Migration::Base" + end + + it do + migration_file.should contain "def up" + end + + it do + migration_file.should contain "def down" + end + end + end + + describe CreateTableMigration do + describe "#render" do + migration = Amber::CLI::Jennifer::CreateTableMigration.new("create_users", %w(name:string owner:ref admin:bool)) + migration_file = MigrationSpecHelper.text_for(migration) + + it do + migration_file.should contain "class CreateUser < Jennifer::Migration::Base" + end + + it do + migration_file.should contain <<-FILE + def up + create_table(:users) do |t| + t.string :name + t.reference :owner + t.bool :admin + t.timestamps + end + end + FILE + end + + it do + migration_file.should contain <<-FILE + def down + drop_table :users + end + FILE + end + end + end + + describe ChangeColumnsMigration do + describe "#render" do + describe "adding columns" do + migration = Amber::CLI::Jennifer::ChangeColumnsMigration.new("add_columns_to_users", %w(name:string owner:ref admin:bool), :add) + migration_file = MigrationSpecHelper.text_for(migration) + + it do + migration_file.should contain "class AddColumnsToUser < Jennifer::Migration::Base" + end + + it do + migration_file.should contain <<-FILE + def up + change_table(:users) do |t| + t.add_column :name, :string + t.add_column :owner, :reference + t.add_column :admin, :bool + end + end + FILE + end + + it do + migration_file.should contain <<-FILE + def down + change_table(:users) do |t| + t.drop_column :name + t.drop_column :owner + t.drop_column :admin + end + end + FILE + end + end + + describe "removing columns" do + migration = Amber::CLI::Jennifer::ChangeColumnsMigration.new("remove_columns_from_users", %w(name:string owner:ref admin:bool), :remove) + migration_file = MigrationSpecHelper.text_for(migration) + + it do + migration_file.should contain "class RemoveColumnsFromUser < Jennifer::Migration::Base" + end + + it do + migration_file.should contain <<-FILE + def up + change_table(:users) do |t| + t.drop_column :name + t.drop_column :owner + t.drop_column :admin + end + end + FILE + end + + it do + migration_file.should contain <<-FILE + def down + change_table(:users) do |t| + t.add_column :name, :string + t.add_column :owner, :reference + t.add_column :admin, :bool + end + end + FILE + end + end + end + end +end diff --git a/spec/amber/cli/templates/migration_spec_helper.cr b/spec/amber/cli/templates/migration_spec_helper.cr index 72766cbb1..7d0e6e261 100644 --- a/spec/amber/cli/templates/migration_spec_helper.cr +++ b/spec/amber/cli/templates/migration_spec_helper.cr @@ -9,7 +9,7 @@ module Amber::CLI ensure `rm -rf ./tmp/db` end - return migration_text + migration_text end def self.sample_migration_for(migration_template_type) diff --git a/spec/amber/cli/templates/sam_spec.cr b/spec/amber/cli/templates/sam_spec.cr new file mode 100644 index 000000000..a00827034 --- /dev/null +++ b/spec/amber/cli/templates/sam_spec.cr @@ -0,0 +1,25 @@ +require "../../../spec_helper" + +module Amber::CLI + module SamSpecHelper + def self.text_for(sam : Sam) : String + sam.render("./tmp/app") + File.read("./tmp/app/sam.cr") + ensure + `rm -rf ./tmp/app` + end + end + + describe Sam do + describe "#render" do + context "with jennifer model" do + sam = Amber::CLI::Sam.new("jennifer") + file = SamSpecHelper.text_for(sam) + + it { file.should contain("Sam.help") } + it { file.should contain(%(require "./db/migrations/*")) } + it { file.should contain(%(load_dependencies "jennifer")) } + end + end + end +end diff --git a/src/amber/cli/commands.cr b/src/amber/cli/commands.cr index e339e913e..8ae9a787c 100644 --- a/src/amber/cli/commands.cr +++ b/src/amber/cli/commands.cr @@ -47,7 +47,7 @@ module Amber::CLI help desc: "# Describe available commands and usages" string ["-t", "--template"], desc: "# Preconfigure for selected template engine. Options: slang | ecr", default: "slang" string ["-d", "--database"], desc: "# Preconfigure for selected database. Options: pg | mysql | sqlite", default: "pg" - string ["-m", "--model"], desc: "# Preconfigure for selected model. Options: granite | crecto", default: "granite" + string ["-m", "--model"], desc: "# Preconfigure for selected model. Options: granite | crecto | jennifer", default: "granite" string ["-r", "--recipe"], desc: "# Use a named recipe. See documentation at https://amberframework.org.", default: nil end end diff --git a/src/amber/cli/commands/generate.cr b/src/amber/cli/commands/generate.cr index 990b51716..f229b57e4 100644 --- a/src/amber/cli/commands/generate.cr +++ b/src/amber/cli/commands/generate.cr @@ -6,7 +6,7 @@ module Amber::CLI class Generate < Command class Options - arg "type", desc: "scaffold, model, controller, migration, mailer, socket, channel, auth, error", required: true + arg "type", desc: "scaffold, model, controller, migration, mailer, socket, channel, auth, error, sam", required: true arg "name", desc: "name of resource", required: false arg_array "fields", desc: "user:reference name:string body:text age:integer published:bool" bool "--no-color", desc: "Disable colored output", default: false @@ -20,34 +20,40 @@ module Amber::CLI def run CLI.toggle_colors(options.no_color?) - if args.type == "error" - template = Template.new("error", ".") + if optional_name? + template = Template.new(args.type.as(String), ".") else ensure_name_argument! if recipe && Amber::Recipes::Recipe.can_generate?(args.type, recipe) template = Amber::Recipes::Recipe.new(args.name, ".", recipe.as(String), args.fields) else - template = Template.new(args.name, ".", args.fields) + ensure_name_argument! + + if recipe && Amber::Recipes::Recipe.can_generate?(args.type, recipe) + template = Amber::Recipes::Recipe.new(args.name, ".", recipe.as(String), args.fields) + else + template = Template.new(args.name, ".", args.fields) + end end end - template.generate args.type + template.generate(args.type) end def recipe CLI.config.recipe end + private def optional_name? + %w(error sam).includes?(args.type) + end + private def ensure_name_argument! unless args.name? error "Parsing Error: The NAME argument is required." exit! help: true, error: true end end - - class Help - caption "# Generate Amber classes" - end end end end diff --git a/src/amber/cli/commands/new.cr b/src/amber/cli/commands/new.cr index 78c08113f..e6f9d48f4 100644 --- a/src/amber/cli/commands/new.cr +++ b/src/amber/cli/commands/new.cr @@ -9,9 +9,10 @@ module Amber::CLI arg "name", desc: "name of project", required: true string "-d", desc: "database", any_of: %w(pg mysql sqlite), default: "pg" string "-t", desc: "template language", any_of: %w(slang ecr), default: "slang" - string "-m", desc: "model type", any_of: %w(granite crecto), default: "granite" + string "-m", desc: "model type", any_of: %w(granite crecto jennifer), default: "granite" string "-r", desc: "recipe" bool "--deps", desc: "installs deps, (shards update)", default: false + bool "--sam", desc: "setup initial Sam tasks file", default: false bool "--no-color", desc: "Disable colored output", default: false help end diff --git a/src/amber/cli/commands/sam.cr b/src/amber/cli/commands/sam.cr new file mode 100644 index 000000000..663241080 --- /dev/null +++ b/src/amber/cli/commands/sam.cr @@ -0,0 +1,35 @@ +require "cli" + +module Amber::CLI + class MainCommand < ::Cli::Supercommand + class Sam < Command + command_name "sam" + + SAM_PATH = "sam.cr" + + class Options + # NOTE: command definition is needed only to be displayed on help output + arg_array "command", desc: "Sam command to be invoked" + unknown + help + end + + class Help + header <<-EOS + Invokes Sam based task. Powered by Sam (https://github.com/imdrasil/sam.cr). + To list all available tasks use: + $ amber sam help + EOS + caption "# Invoke Sam task" + end + + def run + if File.exists?(SAM_PATH) + Helpers.run("crystal run sam.cr -- #{ARGV[1..-1].join(" ")}") + else + exit! "Sam file is not found.", error: true + end + end + end + end +end diff --git a/src/amber/cli/helpers/helpers.cr b/src/amber/cli/helpers/helpers.cr index f7e9641be..d1c825edf 100644 --- a/src/amber/cli/helpers/helpers.cr +++ b/src/amber/cli/helpers/helpers.cr @@ -33,6 +33,10 @@ module Amber::CLI::Helpers File.write("./config/application.cr", application.gsub("require \"amber\"", replacement)) end + def current_timestamp + Time.now.to_s("%Y%m%d%H%M%S%L") + end + def self.run(command, wait = true, shell = true) if wait Process.run(command, shell: shell, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) diff --git a/src/amber/cli/recipes/model.cr b/src/amber/cli/recipes/model.cr index bf8bede11..8eab3c3ec 100644 --- a/src/amber/cli/recipes/model.cr +++ b/src/amber/cli/recipes/model.cr @@ -42,7 +42,7 @@ module Amber::Recipes end def table_name - @table_name ||= "#{Inflector.pluralize(@name)}" + Inflector.pluralize(@name) end end end diff --git a/src/amber/cli/recipes/recipe.cr b/src/amber/cli/recipes/recipe.cr index 1ee021f6e..22caa114a 100644 --- a/src/amber/cli/recipes/recipe.cr +++ b/src/amber/cli/recipes/recipe.cr @@ -23,13 +23,10 @@ module Amber::Recipes def self.can_generate?(template_type, recipe) return false unless ["app", "controller", "model", "scaffold"].includes? template_type - - if recipe.nil? - return false - end + return false if recipe.nil? template = RecipeFetcher.new(template_type, recipe).fetch - template.nil? ? false : true + !template.nil? end def initialize(name : String, directory : String, recipe : String, fields = [] of String) @@ -71,6 +68,8 @@ module Amber::Recipes info "Rendering Scaffold #{name} from #{@recipe}" if model == "crecto" Amber::CLI::CrectoMigration.new(name, @fields).render(directory, list: true, color: true) + elsif model == "jennifer" + Amber::CLI::Jennifer::CreateTableMigration.new("create_#{name}", @fields).render(directory, list: true, color: true) else Amber::CLI::GraniteMigration.new(name, @fields).render(directory, list: true, color: true) end diff --git a/src/amber/cli/templates/app.cr b/src/amber/cli/templates/app.cr index 5df9749eb..93dbf2805 100644 --- a/src/amber/cli/templates/app.cr +++ b/src/amber/cli/templates/app.cr @@ -8,19 +8,41 @@ module Amber::CLI @database_name_base : String @language : String @model : String + @sam : Bool @db_url : String - @wait_for : String @author : String @email : String @github_name : String - def initialize(@name, @database = "pg", @language = "slang", @model = "granite") - @db_url = "" - @wait_for = "" + def initialize(@name, @database, @language, @model, @sam) @database_name_base = generate_database_name_base @author = fetch_author @email = fetch_email @github_name = fetch_github_name + + @db_url = + if @database == "pg" + "postgres://admin:password@db:5432/#{@database_name_base}_development" + elsif @database == "mysql" + "mysql://admin:password@db:3306/#{@database_name_base}_development" + else + "sqlite3:./db/#{@database_name_base}_development.db" + end + end + + def sam? + @sam + end + + def wait_for_command + case @database + when "pg" + "while ! nc -q 1 db 5432 version: '2' services: @@ -30,7 +19,7 @@ services: migrate: build: . image: <%= @name %> - command: bash -c '<%= @wait_for %>amber db migrate seed' + command: bash -c '<%= wait_for_command %>amber db migrate seed' environment: DATABASE_URL: <%= @db_url %> volumes: diff --git a/src/amber/cli/templates/app/shard.yml.ecr b/src/amber/cli/templates/app/shard.yml.ecr index d85961e28..800736862 100644 --- a/src/amber/cli/templates/app/shard.yml.ecr +++ b/src/amber/cli/templates/app/shard.yml.ecr @@ -56,6 +56,11 @@ dependencies: github: crystal-lang/crystal-sqlite3 version: ~> 0.9.0 <% end -%> +<% if sam? -%> + sam: + github: imdrasil/sam.cr + version: ~> 0.2.4 +<% end %> development_dependencies: garnet_spec: diff --git a/src/amber/cli/templates/auth.cr b/src/amber/cli/templates/auth.cr new file mode 100644 index 000000000..c55b0250e --- /dev/null +++ b/src/amber/cli/templates/auth.cr @@ -0,0 +1,128 @@ +require "./field.cr" +require "../helpers/migration" + +module Amber::CLI + abstract class Auth < Teeplate::FileTree + include Helpers + include Helpers::Migration + + @name : String + @fields : Array(Field) + @visible_fields : Array(String) + @database : String = CLI.config.database + @language : String = CLI.config.language + @timestamp : String + @primary_key : String + + def initialize(@name, fields) + @fields = all_fields(fields) + @timestamp = current_timestamp + @primary_key = primary_key + @visible_fields = @fields.reject(&.hidden).map(&.name) + setup_routes + setup_plugs + setup_dependencies + setup_application_controller + end + + def filter(entries) + entries.reject { |entry| entry.path.includes?("src/views") && !entry.path.includes?(".#{@language}") } + end + + private def setup_routes + add_routes :web, <<-ROUTES + get "/profile", #{class_name}Controller, :show + get "/profile/edit", #{class_name}Controller, :edit + patch "/profile", #{class_name}Controller, :update + get "/signin", SessionController, :new + post "/session", SessionController, :create + get "/signout", SessionController, :delete + get "/signup", RegistrationController, :new + post "/registration", RegistrationController, :create + ROUTES + end + + private def setup_plugs + add_plugs :web, <<-PLUGS + plug Authenticate.new + PLUGS + end + + private def setup_dependencies + add_dependencies <<-DEPENDENCY + require "../src/models/**" + require "../src/pipes/**" + DEPENDENCY + end + + private def setup_application_controller + filename = "./src/controllers/application_controller.cr" + controller = File.read(filename) + append_text = "" + + unless controller.includes? "property current_#{@name}" + append_text += current_method_definition + end + + unless controller.includes? "def signed_in?" + append_text += signed_in_method_definition + end + + unless controller.includes? "def redirect_signed_out_#{@name}" + append_text += redirect_signed_out_method_definition + end + + append_text = "#{append_text}\nend\n" + controller = controller.gsub(/end\s*\Z/, append_text) + File.write(filename, controller) + end + + private def current_method_definition + <<-AUTH + + def current_#{@name} + context.current_#{@name} + end\n + AUTH + end + + private def signed_in_method_definition + <<-AUTH + + def signed_in? + current_#{@name} ? true : false + end\n + AUTH + end + + private def redirect_signed_out_method_definition + <<-AUTH + + private def redirect_signed_out_#{@name} + unless signed_in? + flash[:info] = "Must be logged in" + redirect_to "/signin" + end + end + AUTH + end + + private def all_fields(fields) + fields.map { |field| Field.new(field, database: @database) } + + auth_fields + + timestamp_fields + end + + private def auth_fields + %w(email:string hashed_password:password).map do |f| + Field.new(f, hidden: false, database: @database) + end + end + + private def timestamp_fields + %w(created_at:time updated_at:time).map do |f| + Field.new(f, hidden: true, database: @database) + end + end + end +end diff --git a/src/amber/cli/templates/auth/jennifer/spec/controllers/spec_helper.cr b/src/amber/cli/templates/auth/jennifer/spec/controllers/spec_helper.cr new file mode 100644 index 000000000..a3710231b --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/spec/controllers/spec_helper.cr @@ -0,0 +1,2 @@ +require "../spec_helper" +require "garnet_spec" diff --git a/src/amber/cli/templates/auth/jennifer/spec/models/spec_helper.cr b/src/amber/cli/templates/auth/jennifer/spec/models/spec_helper.cr new file mode 100644 index 000000000..cba784c4c --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/spec/models/spec_helper.cr @@ -0,0 +1 @@ +require "../spec_helper" diff --git a/src/amber/cli/templates/auth/jennifer/spec/models/{{name}}_spec.cr.ecr b/src/amber/cli/templates/auth/jennifer/spec/models/{{name}}_spec.cr.ecr new file mode 100644 index 000000000..d3a404ec2 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/spec/models/{{name}}_spec.cr.ecr @@ -0,0 +1,5 @@ +require "./spec_helper" +require "../../src/models/<%= @name %>" + +describe <%= class_name %> do +end diff --git a/src/amber/cli/templates/auth/jennifer/src/controllers/registration_controller.cr.ecr b/src/amber/cli/templates/auth/jennifer/src/controllers/registration_controller.cr.ecr new file mode 100644 index 000000000..f86293e94 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/controllers/registration_controller.cr.ecr @@ -0,0 +1,31 @@ +class RegistrationController < ApplicationController + def new + <%= @name %> = <%= class_name %>.build + render("new.<%= @language %>") + end + + def create + parsed_params = registration_params + <%= @name %> = <%= class_name %>.build(parsed_params) + <%= @name %>.password = parsed_params["password"].as(String?) + + if <%= @name %>.save + session[:<%= @name %>_id] = <%= @name %>.id + flash["success"] = "Created <%= class_name %> successfully." + redirect_to "/" + else + flash["danger"] = "Could not create <%= class_name %>!" + render("new.<%= @language %>") + end + end + + private def registration_params + <%= class_name %>.build_params( + params.validation do + required(:email) { |f| !f.nil? } + required(:password) { |f| !f.nil? } + required(:password_confirmation) { |f| !f.nil? } + end.validate! + ) + end +end diff --git a/src/amber/cli/templates/auth/jennifer/src/controllers/session_controller.cr.ecr b/src/amber/cli/templates/auth/jennifer/src/controllers/session_controller.cr.ecr new file mode 100644 index 000000000..8765334cb --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/controllers/session_controller.cr.ecr @@ -0,0 +1,25 @@ +class SessionController < ApplicationController + def new + <%= @name %> = <%= class_name %>.build + render("new.<%= @language %>") + end + + def create + <%= @name %> = <%= class_name %>.all.where { _email == params["email"].to_s }.first + if <%= @name %> && <%= @name %>.authenticate(params["password"].to_s) + session[:<%= @name %>_id] = <%= @name %>.id + flash[:info] = "Successfully logged in" + redirect_to "/" + else + flash[:danger] = "Invalid email or password" + <%= @name %> = <%= class_name %>.build + render("new.<%= @language %>") + end + end + + def delete + session.delete(:<%= @name %>_id) + flash[:info] = "Logged out. See ya later!" + redirect_to "/" + end +end diff --git a/src/amber/cli/templates/auth/jennifer/src/controllers/{{name}}_controller.cr.ecr b/src/amber/cli/templates/auth/jennifer/src/controllers/{{name}}_controller.cr.ecr new file mode 100644 index 000000000..978d40dff --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/controllers/{{name}}_controller.cr.ecr @@ -0,0 +1,44 @@ +class <%= class_name %>Controller < ApplicationController + before_action do + all { redirect_signed_out_<%= @name %> } + end + + def show + render("show.<%= @language %>") if (<%= @name %> = current_<%= @name %>) + end + + def edit + render("edit.<%= @language %>") if (<%= @name %> = current_<%= @name %>) + end + + def update + <%= @name %> = current_<%= @name %> + if update(<%= @name %>) + flash[:success] = "Updated Profile successfully." + redirect_to "/profile" + elsif <%= @name %> + flash[:danger] = "Could not update Profile!" + render("edit.<%= @language %>") + else + flash[:info] = "Must be logged in" + redirect_to "/signin" + end + end + + private def update(<%= @name %>) + return false unless <%= @name %> && <%= @name %>_params.valid? + params = <%= class_name %>.build_params(<%= @name %>_params.validate!) + <%= @name %>.update_attributes(params) + <%= @name %>.save + end + + private def <%= @name %>_params + params.validation do + required(:email) { |f| !f.nil? && !f.empty? } + optional(:password) { |f| !f.nil? && !f.empty? } + <%- controller_field_names.each do |field| -%> + required(:<%= field %>) { |f| !f.nil? } + <%- end -%> + end + end +end diff --git a/src/amber/cli/templates/auth/jennifer/src/models/{{name}}.cr.ecr b/src/amber/cli/templates/auth/jennifer/src/models/{{name}}.cr.ecr new file mode 100644 index 000000000..ae3a3783b --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/models/{{name}}.cr.ecr @@ -0,0 +1,35 @@ +require "jennifer/model/authentication" + +class <%= class_name %> < Jennifer::Model::Base + include Jennifer::Model::Authentication + + with_authentication + with_timestamps + + mapping( +<% if @fields.empty? -%> + id: Primary64, + email: {type: String, default: ""}, + password_digest: {type: String, default: ""}, + password: Password, + password_confirmation: { type: String?, virtual: true } +<% else -%> + id: <%= id_field.try(&.cr_type) || "Primary64" %>, + email: {type: String, default: ""}, + password_digest: {type: String, default: ""}, + password: Password, + password_confirmation: { type: String?, virtual: true }, +<% user_custom_fields.each do |field| -%> +<% if field.reference? -%> + <%= field.name + "_id" %>: { type: Int32? }, +<% else -%> + <%= field.name %>: { type: <%= field.cr_type %> }, +<% end -%> +<% end -%> +<% end -%> + ) + +<% @fields.select(&.reference?).each do |field| -%> + belongs_to :<%= field.name %>, <%= Inflector.classify(field.name) %> +<% end -%> +end diff --git a/src/amber/cli/templates/auth/jennifer/src/pipes/authenticate.cr.ecr b/src/amber/cli/templates/auth/jennifer/src/pipes/authenticate.cr.ecr new file mode 100644 index 000000000..e5df812aa --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/pipes/authenticate.cr.ecr @@ -0,0 +1,31 @@ +class HTTP::Server::Context + property current_<%= @name %> : <%= class_name %>? +end + +class Authenticate < Amber::Pipe::Base + PUBLIC_PATHS = ["/", "/signin", "/session", "/signup", "/registration"] + + def call(context) + <%= @name %>_id = context.session["<%= @name %>_id"]? + if <%= @name %> = <%= class_name %>.find(<%= @name %>_id) + context.current_<%= @name %> = <%= @name %> + call_next(context) + else + return call_next(context) if public_path?(context.request.path) + context.flash[:warning] = "Please Sign In" + context.response.headers.add "Location", "/signin" + context.response.status_code = 302 + end + end + + private def public_path?(path) + PUBLIC_PATHS.includes?(path) + + # Different strategies can be used to determine if a path is public + # Example, if /admin/* paths are the only private paths + # return false if path.starts_with?("/admin") + # + # Example, if only a few private paths exist + # return false if ["/secret", "/super/secret", "/private"].includes?(path) + end +end diff --git a/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.ecr.ecr new file mode 100644 index 000000000..872a04ed1 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.ecr.ecr @@ -0,0 +1,10 @@ +<%="<"%>%- if (current_<%= @name %> = context.current_<%= @name %>) %> + | Sign Out + <%="<"%>%- active = context.request.path == "/profile" ? "active" : "" %> + %= active %> pull-right" href="/profile"><%="<"%>%= current_<%= @name %>.email %> +<%="<"%>%- else %> + <%="<"%>%- active = context.request.path == "/signup" ? "active" : "" %> + %= active %> pull-right" href="/signup">| Sign Up + <%="<"%>%- active = context.request.path == "/signin" ? "active" : "" %> + %= active %> pull-right" href="/signin">| Sign In +<%="<"%>%- end %> diff --git a/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.slang.ecr b/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.slang.ecr new file mode 100644 index 000000000..adae0e10e --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/layouts/+_nav.slang.ecr @@ -0,0 +1,13 @@ +- if (current_<%= @name %> = context.current_<%= @name %>) + a.nav-item.pull-right href="/signout" + | Sign Out + - active = context.request.path == "/profile" ? "active" : "" + a class="nav-item #{active} pull-right" href="/profile" + == current_<%= @name %>.email +- else + - active = context.request.path == "/signup" ? "active" : "" + a class="nav-item #{active} pull-right" href="/signup" + | Sign Up + - active = context.request.path == "/signin" ? "active" : "" + a class="nav-item #{active} pull-right" href="/signin" + | Sign In diff --git a/src/amber/cli/templates/auth/jennifer/src/views/registration/new.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/registration/new.ecr.ecr new file mode 100644 index 000000000..0cf6e6651 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/registration/new.ecr.ecr @@ -0,0 +1,25 @@ +

Sign Up

+ +<%="<"%>%- if <%= @name %>.errors.any? %> + +<%="<"%>%- end %> + +
+ <%="<"%>%= csrf_tag %> +
+ +
+
+ +
+
+ +
+ +
+
+<%="<"%>%= link_to("Already have an account?", "/signin") -%> diff --git a/src/amber/cli/templates/auth/jennifer/src/views/registration/new.slang.ecr b/src/amber/cli/templates/auth/jennifer/src/views/registration/new.slang.ecr new file mode 100644 index 000000000..7dcb723c2 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/registration/new.slang.ecr @@ -0,0 +1,19 @@ +h1 Sign Up + +- if <%= @name %>.errors + ul.errors + - <%= @name %>.errors.full_messages.each do |error| + li = error + +form action="/registration" method="post" + == csrf_tag + .form-group + input.form-control type="email" name="email" placeholder="Email" + .form-group + input.form-control type="password" name="password" placeholder="Password" + .form-group + input.form-control type="password" name="password_confirmation" placeholder="Password Confirmation" + button.btn.btn-primary.btn-xs type="submit" + | Register +
+== link_to("Already have an account?", "/signin") diff --git a/src/amber/cli/templates/auth/jennifer/src/views/session/edit.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/session/edit.ecr.ecr new file mode 100644 index 000000000..60477eab3 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/session/edit.ecr.ecr @@ -0,0 +1,18 @@ +

Edit Profile

+ +<%="<"%>%- if <%= @name %>.errors.any? %> + +<%="<"%>%- end %> + +
+ <%="<"%>%= csrf_tag %> +
+ %= <%= @name %>.email %>" /> +
+ <%="<"%>%= submit("Update", class: "btn btn-primary btn-xs") %> + <%="<"%>%= link_to("profile", "/profile", class: "btn btn-default btn-xs") %> +
diff --git a/src/amber/cli/templates/auth/jennifer/src/views/session/new.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/session/new.ecr.ecr new file mode 100644 index 000000000..b4cca1d19 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/session/new.ecr.ecr @@ -0,0 +1,22 @@ +

Sign In

+ +<%="<"%>%- if <%= @name %>.errors %> + +<%="<"%>%- end %> + +
+ <%="<"%>%= csrf_tag %> +
+ +
+
+ +
+ +
+
+<%="<"%>%= link_to("Don't have an account yet?", "/signup") -%> diff --git a/src/amber/cli/templates/auth/jennifer/src/views/session/new.slang.ecr b/src/amber/cli/templates/auth/jennifer/src/views/session/new.slang.ecr new file mode 100644 index 000000000..3dd9b407e --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/session/new.slang.ecr @@ -0,0 +1,16 @@ +h1 Sign In + +- if <%= @name %>.errors + ul.errors + - <%= @name %>.errors.full_messages.each do |error| + li = error + +form action="/session" method="post" + == csrf_tag + div.form-group + input.form-control type="email" name="email" placeholder="Email" + div.form-group + input.form-control type="password" name="password" placeholder="Password" + button.btn.btn-primary.btn-xs type="submit" Sign In +
+== link_to("Don't have an account yet?", "/signup") diff --git a/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.ecr.ecr new file mode 100644 index 000000000..60477eab3 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.ecr.ecr @@ -0,0 +1,18 @@ +

Edit Profile

+ +<%="<"%>%- if <%= @name %>.errors.any? %> + +<%="<"%>%- end %> + +
+ <%="<"%>%= csrf_tag %> +
+ %= <%= @name %>.email %>" /> +
+ <%="<"%>%= submit("Update", class: "btn btn-primary btn-xs") %> + <%="<"%>%= link_to("profile", "/profile", class: "btn btn-default btn-xs") %> +
diff --git a/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.slang.ecr b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.slang.ecr new file mode 100644 index 000000000..297a23f3b --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/edit.slang.ecr @@ -0,0 +1,13 @@ +h1 Edit Profile + +- if <%= @name %>.errors + ul.errors + - <%= @name %>.errors.full_messages.each do |error| + li = error + +== form(action: "/profile", method: :patch) do + == csrf_tag + .form-group + input.form-control type="email" name="email" placeholder="Email" value="#{<%= @name %>.email}" + == submit("Update", class: "btn btn-primary btn-xs") + == link_to("profile", "/profile", class: "btn btn-default btn-xs") diff --git a/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.ecr.ecr b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.ecr.ecr new file mode 100644 index 000000000..57a177a87 --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.ecr.ecr @@ -0,0 +1,5 @@ +

Profile

+

+ <%="<"%>%= <%= @name %>.email %> + <%="<"%>%= link_to("edit", "/profile/edit", class: "btn btn-success btn-xs") %> +

diff --git a/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.slang.ecr b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.slang.ecr new file mode 100644 index 000000000..20d99c0db --- /dev/null +++ b/src/amber/cli/templates/auth/jennifer/src/views/{{name}}/show.slang.ecr @@ -0,0 +1,4 @@ +h1 Profile +p + == <%= @name %>.email + == link_to("edit", "/profile/edit", class: "btn btn-success btn-xs") diff --git a/src/amber/cli/templates/crecto_auth.cr b/src/amber/cli/templates/crecto_auth.cr index bac72d45e..bc8bbb650 100644 --- a/src/amber/cli/templates/crecto_auth.cr +++ b/src/amber/cli/templates/crecto_auth.cr @@ -1,129 +1,7 @@ -require "./field.cr" -require "../helpers/migration" +require "./auth" module Amber::CLI - class CrectoAuth < Teeplate::FileTree - include Helpers - include Helpers::Migration + class CrectoAuth < Auth directory "#{__DIR__}/auth/crecto" - - @name : String - @fields : Array(Field) - @visible_fields : Array(String) - @database : String = CLI.config.database - @language : String = CLI.config.language - @timestamp : String - @primary_key : String - - def initialize(@name, fields) - @fields = all_fields(fields) - @timestamp = Time.now.to_s("%Y%m%d%H%M%S%L") - @primary_key = primary_key - @visible_fields = @fields.reject(&.hidden).map(&.name) - setup_routes - setup_plugs - setup_dependencies - setup_application_controller - end - - def filter(entries) - entries.reject { |entry| entry.path.includes?("src/views") && !entry.path.includes?(".#{@language}") } - end - - private def setup_routes - add_routes :web, <<-ROUTES - get "/profile", #{class_name}Controller, :show - get "/profile/edit", #{class_name}Controller, :edit - patch "/profile", #{class_name}Controller, :update - get "/signin", SessionController, :new - post "/session", SessionController, :create - get "/signout", SessionController, :delete - get "/signup", RegistrationController, :new - post "/registration", RegistrationController, :create - ROUTES - end - - private def setup_plugs - add_plugs :web, <<-PLUGS - plug Authenticate.new - PLUGS - end - - private def setup_dependencies - add_dependencies <<-DEPENDENCY - require "../src/models/**" - require "../src/pipes/**" - DEPENDENCY - end - - private def setup_application_controller - filename = "./src/controllers/application_controller.cr" - controller = File.read(filename) - append_text = "" - - unless controller.includes? "property current_#{@name}" - append_text += current_method_definition - end - - unless controller.includes? "def signed_in?" - append_text += signed_in_method_definition - end - - unless controller.includes? "def redirect_signed_out_#{@name}" - append_text += redirect_signed_out_method_definition - end - - append_text = "#{append_text}\nend\n" - controller = controller.gsub(/end\s*\Z/, append_text) - File.write(filename, controller) - end - - private def current_method_definition - <<-AUTH - - def current_#{@name} - context.current_#{@name} - end\n - AUTH - end - - private def signed_in_method_definition - <<-AUTH - - def signed_in? - current_#{@name} ? true : false - end\n - AUTH - end - - private def redirect_signed_out_method_definition - <<-AUTH - - private def redirect_signed_out_#{@name} - unless signed_in? - flash[:info] = "Must be logged in" - redirect_to "/signin" - end - end - AUTH - end - - private def all_fields(fields) - fields.map { |field| Field.new(field, database: @database) } + - auth_fields + - timestamp_fields - end - - private def auth_fields - %w(email:string hashed_password:password).map do |f| - Field.new(f, hidden: false, database: @database) - end - end - - private def timestamp_fields - %w(created_at:time updated_at:time).map do |f| - Field.new(f, hidden: true, database: @database) - end - end end end diff --git a/src/amber/cli/templates/field.cr b/src/amber/cli/templates/field.cr index 8febef0b0..932b49f04 100644 --- a/src/amber/cli/templates/field.cr +++ b/src/amber/cli/templates/field.cr @@ -50,25 +50,36 @@ module Amber::CLI property hidden : Bool property database : String - def initialize(field, hidden = false, database = "pg") + def initialize(field, @hidden = false, database = "pg") field = "#{field}:string" unless field.includes? ":" @name, @type = field.split(":") @database = database @type, @cr_type, @db_type = type_mapping(@type.downcase) - @hidden = hidden end def type_mapping(type = "string") + nilable = false + if type[-1] == '?' + nilable = true + type = type[0...-1] + end + if type_mapping = TYPE_MAPPING[@database]? - if mapping = type_mapping[@type]? - return mapping + if mapping = type_mapping[type]? + return prepare_mapping(mapping, nilable) end end - if mapping = TYPE_MAPPING["common"][@type]? - return mapping - else - raise "type #{@type} not available" - end + mapping = TYPE_MAPPING["common"][type]? + return prepare_mapping(mapping, nilable) if mapping + + raise "type #{type} not available" + end + + def prepare_mapping(mapping, nilable) + return mapping unless nilable + temp_mapping = mapping.clone + temp_mapping[1] += "?" + temp_mapping end def to_json(json : JSON::Builder) @@ -84,7 +95,11 @@ module Amber::CLI end def reference? - self.type == "reference" + type == "reference" + end + + def nilable? + @cr_type[-1] == '?' end end end diff --git a/src/amber/cli/templates/granite_auth.cr b/src/amber/cli/templates/granite_auth.cr index d61099701..ebb2f72e5 100644 --- a/src/amber/cli/templates/granite_auth.cr +++ b/src/amber/cli/templates/granite_auth.cr @@ -1,129 +1,7 @@ -require "./field.cr" -require "../helpers/migration" +require "./auth" module Amber::CLI - class GraniteAuth < Teeplate::FileTree - include Helpers - include Helpers::Migration + class GraniteAuth < Auth directory "#{__DIR__}/auth/granite" - - @name : String - @fields : Array(Field) - @visible_fields : Array(String) - @database : String = CLI.config.database - @language : String = CLI.config.language - @timestamp : String - @primary_key : String - - def initialize(@name, fields) - @fields = all_fields(fields) - @timestamp = Time.now.to_s("%Y%m%d%H%M%S%L") - @primary_key = primary_key - @visible_fields = @fields.reject(&.hidden).map(&.name) - setup_routes - setup_plugs - setup_dependencies - setup_application_controller - end - - def filter(entries) - entries.reject { |entry| entry.path.includes?("src/views") && !entry.path.includes?(".#{@language}") } - end - - private def setup_routes - add_routes :web, <<-ROUTES - get "/profile", #{class_name}Controller, :show - get "/profile/edit", #{class_name}Controller, :edit - patch "/profile", #{class_name}Controller, :update - get "/signin", SessionController, :new - post "/session", SessionController, :create - get "/signout", SessionController, :delete - get "/signup", RegistrationController, :new - post "/registration", RegistrationController, :create - ROUTES - end - - private def setup_plugs - add_plugs :web, <<-PLUGS - plug Authenticate.new - PLUGS - end - - private def setup_dependencies - add_dependencies <<-DEPENDENCY - require "../src/models/**" - require "../src/pipes/**" - DEPENDENCY - end - - private def setup_application_controller - filename = "./src/controllers/application_controller.cr" - controller = File.read(filename) - append_text = "" - - unless controller.includes? "property current_#{@name}" - append_text += current_method_definition - end - - unless controller.includes? "def signed_in?" - append_text += signed_in_method_definition - end - - unless controller.includes? "def redirect_signed_out_#{@name}" - append_text += redirect_signed_out_method_definition - end - - append_text = "#{append_text}\nend\n" - controller = controller.gsub(/end\s*\Z/, append_text) - File.write(filename, controller) - end - - private def current_method_definition - <<-AUTH - - def current_#{@name} - context.current_#{@name} - end\n - AUTH - end - - private def signed_in_method_definition - <<-AUTH - - def signed_in? - current_#{@name} ? true : false - end\n - AUTH - end - - private def redirect_signed_out_method_definition - <<-AUTH - - private def redirect_signed_out_#{@name} - unless signed_in? - flash[:info] = "Must be logged in" - redirect_to "/signin" - end - end - AUTH - end - - private def all_fields(fields) - fields.map { |field| Field.new(field, database: @database) } + - auth_fields + - timestamp_fields - end - - private def auth_fields - %w(email:string hashed_password:password).map do |f| - Field.new(f, hidden: false, database: @database) - end - end - - private def timestamp_fields - %w(created_at:time updated_at:time).map do |f| - Field.new(f, hidden: true, database: @database) - end - end end end diff --git a/src/amber/cli/templates/jennifer_auth.cr b/src/amber/cli/templates/jennifer_auth.cr new file mode 100644 index 000000000..884dfcb0a --- /dev/null +++ b/src/amber/cli/templates/jennifer_auth.cr @@ -0,0 +1,35 @@ +require "./auth" + +module Amber::CLI + class JenniferAuth < Auth + directory "#{__DIR__}/auth/jennifer" + + private def auth_fields + [] of Field + end + + private def id_field + @fields.find { |field| field.name == "id" } + end + + private def user_custom_fields + @fields.select { |field| field.name != "id" } + end + + private def timestamp_fields + %w(created_at:time? updated_at:time?).map do |f| + Field.new(f, hidden: true, database: @database) + end + end + + private def controller_field_names + array = [] of String + timestamp_fields = %w(created_at updated_at) + @fields.each do |f| + next if timestamp_fields.includes?(f.name) + array << (f.reference? ? "#{f.name}_id" : f.name) + end + array + end + end +end diff --git a/src/amber/cli/templates/jennifer_migration.cr b/src/amber/cli/templates/jennifer_migration.cr new file mode 100644 index 000000000..fb3814d08 --- /dev/null +++ b/src/amber/cli/templates/jennifer_migration.cr @@ -0,0 +1,102 @@ +module Amber::CLI + module Jennifer + class Migration < Amber::CLI::Migration + directory "#{__DIR__}/migration/jennifer/empty" + + NEW_TABLE_REGEXP = /^create_([a-z][\w_\d]*)$/ + ADD_COLUMNS_REGEXP = /^add_.*_to_([a-z][\w_\d]*)$/ + REMOVE_COLUMNS_REGEXP = /^remove_.*_from_([a-z][\w_\d]*)$/ + + getter table_name : String? + + def self.build(name, fields) + type = extract_type(name, fields) + if type == :unspecified + self.new(name, fields) + elsif type == :new + CreateTableMigration.new(name, fields) + else + ChangeColumnsMigration.new(name, fields, type) + end + end + + private def self.extract_type(class_name, fields) + if fields.empty? && !class_name.match(NEW_TABLE_REGEXP) + :unspecified + else + case class_name + when NEW_TABLE_REGEXP + :new + when ADD_COLUMNS_REGEXP + :add + when REMOVE_COLUMNS_REGEXP + :remove + else + :unspecified + end + end + end + + def up_definition; end + def down_definition; end + + private def field_type(field) + case field.type + when "boolean" + "bool" + else + field.type + end + end + + private def null_statement(field) + field.nilable? ? ", {:null => true}" : "" + end + + private def extra_fields + [] of Field + end + end + + class CreateTableMigration < Migration + directory "#{__DIR__}/migration/jennifer/create_table" + + def initialize(name, fields) + super(name, fields) + @table_name = name.match(NEW_TABLE_REGEXP).not_nil![1] + end + + private def column_definition(field) + "t.#{field_type(field)} :#{field.name}#{null_statement(field)}" + end + end + + class ChangeColumnsMigration < Migration + directory "#{__DIR__}/migration/jennifer/change_columns" + + @type : Symbol + + def initialize(name, fields, @type) + raise ArgumentError.new unless [:add, :remove].includes?(@type) + super(name, fields) + @table_name = name.match(add? ? ADD_COLUMNS_REGEXP : REMOVE_COLUMNS_REGEXP).not_nil![1] + end + + def add? + @type == :add + end + + def remove? + @type == :remove + end + + private def add_column(field) + "t.add_column :#{field.name}, :#{field_type(field)}#{null_statement(field)}" + end + + private def remove_column(field) + "t.drop_column :#{field.name}" + end + end + end +end diff --git a/src/amber/cli/templates/migration.cr b/src/amber/cli/templates/migration.cr index 608c32322..b6042f97e 100644 --- a/src/amber/cli/templates/migration.cr +++ b/src/amber/cli/templates/migration.cr @@ -14,12 +14,15 @@ module Amber::CLI @primary_key : String def initialize(@name, fields) - @timestamp = Time.now.to_s("%Y%m%d%H%M%S%L") - @fields = fields.map { |field| Field.new(field, database: @database) } - @fields += %w(created_at:time updated_at:time).map do |f| + @timestamp = current_timestamp + @fields = fields.map { |field| Field.new(field, database: @database) } + extra_fields + @primary_key = primary_key + end + + private def extra_fields + %w(created_at:time updated_at:time).map do |f| Field.new(f, hidden: true, database: @database) end - @primary_key = primary_key end end end diff --git a/src/amber/cli/templates/migration/jennifer/change_columns/db/migrations/{{timestamp}}_{{name}}.cr.ecr b/src/amber/cli/templates/migration/jennifer/change_columns/db/migrations/{{timestamp}}_{{name}}.cr.ecr new file mode 100644 index 000000000..5374127b9 --- /dev/null +++ b/src/amber/cli/templates/migration/jennifer/change_columns/db/migrations/{{timestamp}}_{{name}}.cr.ecr @@ -0,0 +1,29 @@ +class <%= class_name %> < Jennifer::Migration::Base + def up + change_table(:<%= @table_name %>) do |t| +<% if add? -%> +<% @fields.each do |field| -%> + <%= add_column(field) %> +<% end -%> +<% else -%> +<% @fields.each do |field| -%> + <%= remove_column(field) %> +<% end -%> +<% end -%> + end + end + + def down + change_table(:<%= @table_name %>) do |t| +<% if remove? -%> +<% @fields.each do |field| -%> + <%= add_column(field) %> +<% end -%> +<% else -%> +<% @fields.each do |field| -%> + <%= remove_column(field) %> +<% end -%> +<% end -%> + end + end +end diff --git a/src/amber/cli/templates/migration/jennifer/create_table/db/migrations/{{timestamp}}_{{name}}.cr.ecr b/src/amber/cli/templates/migration/jennifer/create_table/db/migrations/{{timestamp}}_{{name}}.cr.ecr new file mode 100644 index 000000000..d96b6cef0 --- /dev/null +++ b/src/amber/cli/templates/migration/jennifer/create_table/db/migrations/{{timestamp}}_{{name}}.cr.ecr @@ -0,0 +1,14 @@ +class <%= class_name %> < Jennifer::Migration::Base + def up + create_table(:<%= @table_name %>) do |t| +<% @fields.each do |field| -%> + <%= column_definition(field) %> +<% end -%> + t.timestamps + end + end + + def down + drop_table :<%= @table_name %> + end +end diff --git a/src/amber/cli/templates/migration/jennifer/empty/db/migrations/{{timestamp}}_{{name}}.cr.ecr b/src/amber/cli/templates/migration/jennifer/empty/db/migrations/{{timestamp}}_{{name}}.cr.ecr new file mode 100644 index 000000000..237a40592 --- /dev/null +++ b/src/amber/cli/templates/migration/jennifer/empty/db/migrations/{{timestamp}}_{{name}}.cr.ecr @@ -0,0 +1,9 @@ +class <%= class_name %> < Jennifer::Migration::Base + def up + # Put here any code expected to be invoked during migration + end + + def down + # Put here any code expected to be invoked during rollback + end +end diff --git a/src/amber/cli/templates/model.cr b/src/amber/cli/templates/model.cr index c81fe1f5b..50ce20fc3 100644 --- a/src/amber/cli/templates/model.cr +++ b/src/amber/cli/templates/model.cr @@ -9,12 +9,11 @@ module Amber::CLI @name : String @fields : Array(Field) @database : String = CLI.config.database + @table_name : String? def initialize(@name, fields) @fields = fields.map { |field| Field.new(field, database: @database) } - @fields += %w(created_at:time updated_at:time).map do |f| - Field.new(f, hidden: true, database: @database) - end + @fields += extra_fields add_dependencies <<-DEPENDENCY require "../src/models/**" @@ -22,7 +21,13 @@ module Amber::CLI end def table_name - @table_name ||= "#{Inflector.pluralize(@name)}" + @table_name ||= Inflector.pluralize(@name) + end + + private def extra_fields + %w(created_at:time updated_at:time).map do |f| + Field.new(f, hidden: true, database: @database) + end end end end diff --git a/src/amber/cli/templates/sam.cr b/src/amber/cli/templates/sam.cr new file mode 100644 index 000000000..b64bf6905 --- /dev/null +++ b/src/amber/cli/templates/sam.cr @@ -0,0 +1,14 @@ +require "./field.cr" + +module Amber::CLI + class Sam < Teeplate::FileTree + include Amber::CLI::Helpers + + directory "#{__DIR__}/sam" + + @model : String + + def initialize(@model) + end + end +end diff --git a/src/amber/cli/templates/sam/sam.cr.ecr b/src/amber/cli/templates/sam/sam.cr.ecr new file mode 100644 index 000000000..5a36ac850 --- /dev/null +++ b/src/amber/cli/templates/sam/sam.cr.ecr @@ -0,0 +1,29 @@ +require "./config/*" +require "sam" +<% if @model == "jennifer" -%> +require "./db/migrations/*" + +load_dependencies "jennifer" +<% end -%> + +# +# This is a Sam file. Place your tasks here or in separate files and load them +# explicitly. To load any package defined Sam task - use: +# +# load_dependencies "package_name" +# +# Here is an example of simple Sam task: +# +# Sam.namespace "simon" do +# task "says" do |t, args| +# puts args[0] +# end +# end +# +# To invoke this task use: +# $ amber sam simon:says "Get happy codding with Amber" +# +# For the following reading visit the https://github.com/imdrasil/sam.cr. +# + +Sam.help diff --git a/src/amber/cli/templates/template.cr b/src/amber/cli/templates/template.cr index fdadbc875..454acc7c2 100644 --- a/src/amber/cli/templates/template.cr +++ b/src/amber/cli/templates/template.cr @@ -5,6 +5,7 @@ require "./app" require "./migration" require "./crecto_migration" require "./granite_migration" +require "./jennifer_migration" require "./model" require "./crecto_model" require "./granite_model" @@ -17,7 +18,9 @@ require "./socket" require "./channel" require "./crecto_auth" require "./granite_auth" +require "./jennifer_auth" require "./error" +require "./sam" module Amber::CLI class Template @@ -44,17 +47,11 @@ module Amber::CLI def generate(template : String, options = nil) case template when "app" - if options - info "Rendering App #{name} in #{directory}" - App.new(name, options.d, options.t, options.m).render(directory, list: true, color: true) - if options.deps? - info "Installing Dependencies" - Helpers.run("cd #{name} && shards update") - end - end + info "Rendering App #{name} in #{directory}" + generate_app(options) when "migration" info "Rendering Migration #{name}" - Migration.new(name, fields).render(directory, list: true, color: true) + generate_migration when "model" info "Rendering Model #{name}" if model == "crecto" @@ -95,15 +92,13 @@ module Amber::CLI WebSocketChannel.new(name).render(directory, list: true, color: true) when "auth" info "Rendering Auth #{name}" - if model == "crecto" - CrectoAuth.new(name, fields).render(directory, list: true, color: true) - else - GraniteAuth.new(name, fields).render(directory, list: true, color: true) - end + generate_authentication when "error" info "Rendering Error Template" actions = ["forbidden", "not_found", "internal_server_error"] ErrorTemplate.new("error", actions).render(directory, list: true, color: true) + when "sam" + generate_sam else CLI.logger.error "Template not found", "Generate", :light_red end @@ -120,6 +115,47 @@ module Amber::CLI def error(msg) CLI.logger.error msg, "Generate", :red end + + private def generate_authentication + case model + when "crecto" + CrectoAuth.new(name, fields).render(directory, list: true, color: true) + when "jennifer" + JenniferAuth.new(name, fields).render(directory, list: true, color: true) + else + GraniteAuth.new(name, fields).render(directory, list: true, color: true) + end + end + + private def generate_app(options) + return unless options + info "Rendering App #{name} in #{directory}" + App.new(name, options.d, options.t, options.m, options.sam?).render(directory, list: true, color: true) + generate_sam if options.sam? + if options.deps? + info "Installing Dependencies" + Helpers.run("cd #{name} && shards update") + end + end + + private def generate_sam(options = nil) + if File.exists?(File.join(directory, MainCommand::Sam::SAM_PATH)) + info "Sam file is already exists." + else + info "Rendering Sam file" + Sam.new(options.try(&.m) || model).render(directory, list: true, color: true) + end + end + + def generate_migration(migration_name = name) + migration_instance = + if model == "jennifer" + Jennifer::Migration.build(migration_name, fields) + else + Migration.new(migration_name, fields) + end + migration_instance.render(directory, list: true, color: true) + end end end @@ -166,7 +202,7 @@ module Teeplate end def class_name - @class_name ||= @name.camelcase + @class_name ||= Inflector.classify(@name) end def display_name diff --git a/src/amber/environment/settings.cr b/src/amber/environment/settings.cr index 3157f22ad..8329f57bc 100644 --- a/src/amber/environment/settings.cr +++ b/src/amber/environment/settings.cr @@ -65,6 +65,7 @@ module Amber::Environment }}, ssl_key_file: {type: String?, default: nil}, ssl_cert_file: {type: String?, default: nil}, + local_time_zone: {type: String?, default: nil}, smtp: { type: Hash(String, SettingValue), getter: false,