class Fanforce::AppFactory::CLI::ScaffoldingFile include Fanforce::AppFactory::CLI::Utils require 'digest' require 'multi_json' require_gem 'diffy', 'diffy' attr_reader :scaffold_filepath, :app def initialize(scaffold_filepath, app=nil) @scaffold_filepath = scaffold_filepath @app = app if app @filepath = "#{app.dir}/" + scaffold_filepath.gsub("#{Fanforce::AppFactory::CLI::Scaffolding.dir}/", '') @relativepath = extract_relativepath(@filepath) end end def create if File.directory?(scaffold_filepath) mkdir return nil end return update if File.exists?(filepath) contents = File.open(scaffold_filepath).read File.open(filepath, 'w') {|f| f.write(cleanse_content(contents)) } log "#{'Created'.format(:green,:bold)} #{relativepath}" return true end def update if File.directory?(scaffold_filepath) mkdir return nil end is_missing = is_missing? if !is_missing return if is_corrupted? return if is_forked? return if is_current_version? end content = updated_content File.open(filepath, 'w') {|f| f.write(content) } log "#{(is_missing ? 'Created' : 'Updated').format(:green,:bold)} #{relativepath}" return true end def status return @status unless @status.nil? if is_missing? @status = :missing elsif is_corrupted? @status = :corrupted elsif is_forked? @status = :forked elsif is_previous_version? @status = :previous elsif is_diverged? @status = :diverged elsif is_current_version? @status = :current else error "Unknown status for #{relativepath}" end end def diff_changes return nil if scaffold_filepath =~ /\/Routes\.rb$/ return nil if is_missing? return nil if is_image_file? original_contents = File.open(filepath).read Diffy::Diff.new(original_contents, updated_content).to_s(:text) end ###################################################################################################################### def mkdir Dir.mkdir(filepath) if !File.directory?(filepath) end def extension @extension ||= scaffold_filepath.match(/([^\/\.]+)$/)[1].to_s.downcase end def extension_type @extension_type ||= extension.downcase.to_sym end ###################################################################################################################### def scaffold_type(filepath=scaffold_filepath) # :file, :image, :partial, :items, :json, :skipped @scaffold_types ||= {} return @scaffold_types[filepath] unless @scaffold_types[filepath].nil? return if !File.exists?(filepath) if is_image_file? type = :image elsif filepath =~ /\/.+\.json$/ type = :json else first_line = File.open(filepath).read.split("\n").first if first_line matches = first_line.match(/\s*\S+\s*AppFactoryScaffolding\s*:\s*(FILE|PARTIAL|ITEMS|SKIPPED)/) type = matches[1].downcase.to_sym if matches and matches[1] end end raise "scaffold_type could not be read from file: #{filepath}" if !type raise "scaffold_type (#{type}) is unknown: #{lines.first}" if ![:file, :image, :partial, :items, :json, :skipped].include?(type) @scaffold_types[filepath] = type end ###################################################################################################################### def is_missing? return @is_missing unless @is_missing.nil? @is_missing = !File.exists?(filepath) end def is_corrupted? return @is_corrupted unless @is_corrupted.nil? scaffold_type = scaffold_type(filepath) if !scaffold_type error "The first line of #{relativepath} should specify an AppFactoryScaffolding type" elsif scaffold_type == :json begin MultiJson.load(File.open(filepath).read) rescue Exception => e; error "Corrupted #{relativepath}: #{e.message}"; end elsif scaffold_type == :partial File.open(filepath).read.match(/^\s*\S+\s*END PARTIAL/) end @is_corrupted = false end def is_previous_version? return @is_previous_version unless @is_previous_version.nil? return @is_previous_version = false if is_current_version? # :file, :image, :partial, :items, :json, :skipped if [:file,:image,:partial].include?(scaffold_type) @is_previous_version = digest_exists_in_registry?(current_digest) elsif [:items].include?(scaffold_type) list_exists_in_registry?(File.open(filepath).read) elsif [:json].include?(scaffold_type) json_exists_in_registry?(File.open(filepath).read) elsif [:skipped].include?(scaffold_type) return @is_previous_version = false else raise 'unknown scaffold_type' end end def is_forkable? return @is_forkable unless @is_forkable.nil? @is_forkable = !(scaffold_filepath =~ /\/(Routes\.rb|config\.json|.gitignore)$/) end def is_image_file? return @is_image_file unless @is_image_file.nil? @is_image_file = scaffold_filepath =~ /\.(png|jpg|gif|ico)$/ end def is_forked? return @is_forked unless @is_forked.nil? return @is_forked = true if is_image_file? and !is_current_version? and !is_previous_version? return @is_forked = false if !is_forkable? @is_forked = File.open(filepath, &:readline).include?('FORKED') end def is_diverged? return @is_diverged unless @is_diverged.nil? @is_diverged = !is_current_version? end def is_current_version? return @is_current unless @is_current.nil? # :file, :image, :partial, :items, :json, :skipped if [:file,:image].include?(scaffold_type) @is_current = (current_digest == scaffold_digest) elsif [:partial].include?(scaffold_type) @is_current = (current_digest == updated_digest) elsif [:items].include?(scaffold_type) @is_current = list_matches_latest_registry?(File.open(filepath).read) elsif [:json].include?(scaffold_type) @is_current = json_matches_latest_registry?(File.open(filepath).read) elsif [:skipped].include?(scaffold_type) @is_current = true else raise 'unknown scaffold_type' end end ###################################################################################################################### def registry_filepath scaffold_fileparts = scaffold_filepath.split('/') "#{scaffold_fileparts[0...-1].join('/')}/._#{scaffold_fileparts[-1]}.registry" end def digest_exists_in_registry?(digest) return false if !File.exists?(registry_filepath) File.open(registry_filepath, 'r').each do |line| version, previous_digest = line.strip.split(/:\s?/) return true if digest == previous_digest end return false end def list_registry_rules(&block) return @list_registry_rules unless @list_registry_rules.nil? or block rules = {ADD: [], REM: [], OPT: []} File.open(registry_filepath).each_line do |line| rule_changes = MultiJson.load(line.split(':', 2)[1], symbolize_keys: true) rule_changes[:ADD].each do |item| item = item.strip rules[:ADD] << item unless rules[:ADD].include?(item) rules[:REM].delete(item) rules[:OPT].delete(item) end if rule_changes[:ADD] rule_changes[:REM].each do |item| item = item.strip rules[:ADD].delete(item) rules[:REM] << item unless rules[:REM].include?(item) rules[:OPT].delete(item) end if rule_changes[:REM] rule_changes[:OPT].each do |item| item = item.strip rules[:ADD].delete(item) rules[:REM].delete(item) rules[:OPT] << item unless rules[:OPT].include?(item) end if rule_changes[:OPT] block.call(rules) if block end @list_registry_rules = rules end def list_matches_latest_registry?(contents) raise if scaffold_type != :items list = [] contents = contents.is_a?(Array) ? contents : contents.split("\n") contents.each do |line| line = line.strip list << line if line.present? and line !~ /^\s*#/ end list_registry_rules[:ADD].each do |item| return false if !list.include?(item) end list_registry_rules[:REM].each do |item| return false if list.include?(item) end return true end def list_exists_in_registry?(contents) raise if scaffold_type != :items list = [] contents = contents.is_a?(Array) ? contents : contents.split("\n") contents.each do |line| line = line.strip list << line if line.present? and line !~ /^\s*#/ end list_registry_rules do |rules| rules[:ADD].each do |item| next if !list.include?(item) end rules[:REM].each do |item| next if list.include?(item) end return true end return false end def json_registry_rules(&block) return @json_registry_rules unless @json_registry_rules.nil? or block rules = {ADD: {}, REM: {}, OPT: {}} File.open(registry_filepath).each_line do |line| rule_changes = MultiJson.load(line.split(':', 2)[1], symbolize_keys: true) update_json_registry_rules(:ADD, rule_changes[:ADD], rules, '.') if rule_changes[:ADD] update_json_registry_rules(:REM, rule_changes[:REM], rules, '.') if rule_changes[:REM] update_json_registry_rules(:OPT, rule_changes[:OPT], rules, '.') if rule_changes[:OPT] block.call(rules) if block end @json_registry_rules = rules end def update_json_registry_rules(type, rule_changes, rules, key_path) rule_changes.each do |rule_key, rule_value| next update_json_registry_rules(type, rule_value, rules, "#{key_path}.#{rule_key}") if rule_value.is_a?(Hash) if type == :ADD json_rule(rules, :ADD, key_path, true)[rule_key] = rule_value (rule = json_rule rules, :REM, key_path) && rule.delete(rule_key) (rule = json_rule rules, :OPT, key_path) && rule.delete(rule_key) elsif type == :REM (rule = json_rule rules, :ADD, key_path) && rule.delete(rule_key) json_rule(rules, :REM, key_path, true)[rule_key] = rule_value (rule = json_rule rules, :OPT, key_path) && rule.delete(rule_key) elsif type == :OPT (rule = json_rule rules, :ADD, key_path) && rule.delete(rule_key) (rule = json_rule rules, :REM, key_path) && rule.delete(rule_key) json_rule(rules, :OPT, key_path, true)[rule_key] = rule_value end end end def json_rule(rules, type, key_path, create_hash_if_nil=false) rule = rules[type] key_path.gsub(/\.\./, '.').split('.').each do |key| next if key.blank? rule[key.to_sym] ||= {} if create_hash_if_nil rule = rule[key.to_sym] end rule end def json_matches_latest_registry?(json) hash = MultiJson.load(json, symbolize_keys: true) subtract_hash_from_hash!(json_registry_rules[:OPT], hash) return false if !hash_keys_match_hash_keys(json_registry_rules[:ADD], hash) return false if hash_keys_overlap_hash_keys(json_registry_rules[:REM], hash) return true end def json_exists_in_registry?(json) original_hash = MultiJson.load(json, symbolize_keys: true) json_registry_rules do |rules| hash = original_hash.clone subtract_hash_from_hash!(rules[:OPT], hash) next if !hash_keys_match_hash_keys(rules[:ADD], hash) next if hash_keys_overlap_hash_keys(rules[:REM], hash) return true end return false end def subtract_hash_from_hash!(hash_to_subtract, hash_to_retain) hash_to_subtract.each do |k,v| next subtract_hash_from_hash!(v, hash_to_retain[k]) if v.is_a?(Hash) and hash_to_retain[k].is_a?(Hash) hash_to_retain.delete(k) if hash_to_retain.is_a?(Hash) end end def hash_keys_match_hash_keys(hash1, hash2) hash1.each do |k,v| return false if !hash_keys_match_hash_keys(v, hash2[k]) if v.is_a?(Hash) and hash2[k].is_a?(Hash) return false if v.is_a?(Hash) and !hash2[k].is_a?(Hash) return false if !hash2.has_key?(k) end return true end def hash_keys_overlap_hash_keys(hash1, hash2) hash1.each do |k,v| return true if hash_keys_overlap_hash_keys(v, hash2[k]) if v.is_a?(Hash) and hash2[k].is_a?(Hash) return true if hash2.has_key?(k) end return false end def current_digest return @current_digest unless @current_digest.nil? if [:image,:file,:partial].include?(scaffold_type) @current_digest = Digest::SHA1.file(filepath).hexdigest.strip else raise "There is no digest scaffold type: #{scaffold_type}" end end def scaffold_digest return @scaffold_digest unless @scaffold_digest.nil? if [:image,:file].include?(scaffold_type) @scaffold_digest = Digest::SHA1.file(scaffold_filepath).hexdigest.strip elsif [:partial].include?(scaffold_type) @scaffold_digest = Digest::SHA1.hexdigest(extract_partial(File.open(scaffold_filepath).read, scaffold_filepath)).strip elsif [:items].include?(scaffold_type) @scaffold_digest = Digest::SHA1.hexdigest(extract_items_from_scaffold).strip else raise "There is no digest scaffold type: #{scaffold_type}" end end def updated_digest return @updated_digest unless @updated_digest.nil? @updated_digest = Digest::SHA1.hexdigest(updated_content).strip end def app_factory_line_for_gemfile line = "gem 'fanforce-app-factory'" return line if !config[:app_factory_gem].is_a?(Hash) line += ", '#{config[:app_factory_gem][:version]}'" if config[:app_factory_gem][:version].present? line += ", :path => '#{config[:app_factory_gem][:path]}'" if config[:app_factory_gem][:path].present? line += ", :git => '#{config[:app_factory_gem][:git]}'" if config[:app_factory_gem][:git].present? line end ###################################################################################################################### def extract_partial(content, filepath) raise if scaffold_type != :partial partial = '' found_end = false content.lines.each do |line| break found_end = true if is_line_ending_partial?(line) partial += line end raise "END PARTIAL could not be found for #{extract_relativepath(filepath)}" if !found_end return partial end def extract_post_partial(content, filepath) raise if scaffold_type != :partial post_partial = '' found_end = false content.lines.each do |line| found_end ||= is_line_ending_partial?(line) || next post_partial += line end error "END PARTIAL could not be found for #{extract_relativepath(filepath)}" if !found_end return post_partial end def is_line_ending_partial?(line) return true if extension_type == :haml and line =~ /^\s*-#\s*END PARTIAL/ return true if [:css,:scss,:js].include?(extension_type) and line =~ /^\s*\/\/\s*END PARTIAL/ return true if [:gemfile,:rb,:gitignore,:ru,:rakefile,:coffee,:txt].include?(extension_type) and line =~ /^\s*#\s*END PARTIAL/ end def updated_content if is_missing? or [:file,:image].include?(scaffold_type) File.open(scaffold_filepath).read elsif [:partial].include?(scaffold_type) scaffold_partial = extract_partial(File.open(scaffold_filepath).read, scaffold_filepath) current_post_partial = extract_post_partial(File.open(filepath).read, filepath) cleanse_content(scaffold_partial + current_post_partial) elsif [:items].include?(scaffold_type) content = '' items_to_add = list_registry_rules[:ADD].clone items_to_rem = list_registry_rules[:REM].clone File.open(filepath).read.lines do |line| item = line.strip content += line if !items_to_rem.include?(item) items_to_add.delete(item) end content += "\n" if content !~ /\n$/ items_to_add.each do |item| content += "#{item}\n" end cleanse_content(content) elsif [:json].include?(scaffold_type) hash = MultiJson.load(File.open(filepath).read, symbolize_keys: true) json_registry_rules[:ADD].each {|k,v| hash[k] = v if !hash.has_key?(k) } json_registry_rules[:REM].each {|k,v| hash.delete(k) } cleanse_content(hash) end end ###################################################################################################################### def cleanse_content(contents) if scaffold_filepath =~ /\/config\.json$/ cleanup_config_json(contents) elsif scaffold_filepath =~ /\/Gemfile$/ cleanup_gemfile(contents) else contents end end def cleanup_config_json(contents) hash = contents.is_a?(Hash) ? contents : MultiJson.load(contents, symbolize_keys: true) hash[:_id] ||= app._id hash[:name] ||= app._id.titleize hash[:description] ||= 'This is where you put a short app description.' hash[:fanforce_version_dependency] = config[:fanforce_version_dependency] hash[:created_with_app_factory_version] = Fanforce::AppFactory::VERSION cleaned = JSON.pretty_generate(hash) return cleaned end def cleanup_gemfile(contents) contents.gsub('gem FANFORCE-APP-FACTORY', app_factory_line_for_gemfile) end ###################################################################################################################### def filepath @filepath || (raise 'app is not set' if !app) end def relativepath @relativepath || (raise 'app is not set' if !app) end def extract_relativepath(filepath) filepath.gsub("#{Fanforce::CLI::DIR}/", '') end end