require "scaffolding" require "scaffolding/file_manipulator" require "faraday" require "tempfile" namespace :bullet_train do namespace :api do desc "Bump the current version of application's API" task :bump_version do # Calculate new version. initializer_content = File.new("config/initializers/api.rb").readline previous_version = initializer_content.scan(/v\d+/).pop new_version_int = previous_version.scan(/\d+/).pop.to_i + 1 new_version = "v#{new_version_int}" # Update initializer. File.write("config/initializers/api.rb", initializer_content.gsub(previous_version, new_version)) [ "app/controllers/api/#{new_version}", "app/views/api/#{new_version}", "test/controllers/api/#{new_version}" ].each do |dir| Dir.mkdir(dir) end files_to_update = [ "config/routes/api/#{previous_version}.rb", Dir.glob("app/controllers/api/#{previous_version}/**/*.rb") + Dir.glob("app/views/api/#{previous_version}/**/*.json.jbuilder") + Dir.glob("test/controllers/api/#{previous_version}/**/*.rb") ].flatten files_to_update.each do |file_name| previous_file_contents = File.open(file_name).readlines new_file_name = file_name.gsub(previous_version, new_version) updated_file_contents = previous_file_contents.map do |line| if line.match?(previous_version) line.gsub(previous_version, new_version) else line.gsub("Api::#{previous_version.upcase}", "Api::#{new_version.upcase}") end end # We can't create new files unless each directory under #{api/previous_version} # has been created under the new api directory. For example, we have to create # the `projects` directory before we can create the file `api/v2/projects/pages.json.jbuilder.` new_version_dir, dir_hierarchy = new_file_name.split(/(?<=#{new_version})\//) if dir_hierarchy.present? && dir_hierarchy.match?("/") dir_hierarchy = dir_hierarchy.split("/") dir_hierarchy.inject(new_version_dir) do |base, child_dir_or_file| # Stop making new directories if the string has an extention like ".rb" break if child_dir_or_file.match?(/\./) new_hierarchy = "#{base}/#{child_dir_or_file}" Dir.mkdir(new_hierarchy) unless Dir.exist?(new_hierarchy) new_hierarchy end end Scaffolding::FileManipulator.write(new_file_name, updated_file_contents) end # Here we make sure config/api/#{new_version}.rb is called from within the main routes file. previous_file_contents = File.open("config/routes.rb").readlines updated_file_contents = previous_file_contents.map do |line| if line.match?("draw \"api/#{previous_version}\"") new_version_draw_line = line.gsub(previous_version, new_version) line + new_version_draw_line else line end end Scaffolding::FileManipulator.write("config/routes.rb", updated_file_contents) # Update application locale for each locale that exists. I18n.available_locales.each do |lang| file = "config/locales/#{lang}/application.#{lang}.yml" transformer = Scaffolding::Transformer.new("", "") if File.exist?(file) transformer.add_line_to_file( file, "#{new_version_int}: #{new_version.upcase}", Scaffolding::Transformer::RUBY_NEW_API_VERSION_HOOK, prepend: true ) end end puts "Finished bumping to #{new_version}" end desc "Push OpenAPI yaml file to Redocly" task push_to_redocly: :environment do include Rails.application.routes.url_helpers raise "You need to set REDOCLY_ORGANIZATION_ID in your environment. You can fetch it from the URL when you're on your Redocly dashboard." unless ENV["REDOCLY_ORGANIZATION_ID"].present? raise "You need to set REDOCLY_API_KEY in your environment. You can create one at https://app.redocly.com/org/#{ENV["REDOCLY_ORGANIZATION_ID"]}/settings/api-keys ." unless ENV["REDOCLY_API_KEY"].present? # Create a new Faraday connection conn = Faraday.new(api_url(version: BulletTrain::Api.current_version)) # Fetch the file response = conn.get # Check if the request was successful if response.status == 200 # Create a temp file temp_file = Tempfile.new(["openapi-", ".yaml"]) # Write the file content to the temp file temp_file.binmode temp_file.write(response.body) temp_file.rewind # Close and delete the temp file when the script exits temp_file.close puts "File downloaded and saved to: #{temp_file.path}" puts `echo "#{ENV["REDOCLY_API_KEY"]}" | redocly login` puts `redocly push #{temp_file.path} "@#{ENV["REDOCLY_ORGANIZATION_ID"]}/#{I18n.t("application.name")}@#{BulletTrain::Api.current_version}" --public --upsert` temp_file.unlink else puts "Failed to download the OpenAPI Document. Status code: #{response.status}" end end desc "Export the OpenAPI schema for the application" task export_openapi_schema: :environment do @version = BulletTrain::Api.current_version dir = "tmp/openapi" Dir.mkdir(dir) unless File.exist?(dir) File.open("#{dir}/openapi-#{Time.now.strftime("%Y%m%d-%H%M%S")}.yaml", "w+") do |f| f.binmode f.write( ApplicationController.renderer.render( template: "api/#{@version}/open_api/index", layout: false, format: :text, assigns: {version: @version} ) ) end end desc "Create `api_title` and `api_description` translations" task create_translations: :environment do # Define the root of the locales directory locales_root = Rails.root.join("config", "locales").to_s # Define the backup directory backup_dir = Rails.root.join("tmp", "locales_backup") FileUtils.mkdir_p(backup_dir) unless Dir.exist?(backup_dir) original_files = Dir.glob("#{locales_root}/**/*.yml") differing_files_count = 0 original_files.each do |file| puts "Processing #{file}..." lines = File.readlines(file) new_content = [] inside_fields = false current_field = nil fields_indentation = 4 api_title_exists = false api_description_exists = false # Read the entire file as a string lines.each_with_index do |line, index| # fields are always under locale:model: if line.start_with?("#{" " * fields_indentation}fields:") inside_fields = true new_content << line next end if inside_fields current_indentation = line.index(/\S/) || 0 if current_indentation <= fields_indentation # We've exited the fields block inside_fields = false current_field = nil elsif current_indentation == fields_indentation + 2 # We're on a line with a field name current_field = line.strip.split(":").first api_title_exists = false api_description_exists = false n = 1 until index + n >= lines.length || (lines[index + n].index(/\S/) || 0) <= fields_indentation if lines[index + n].strip.start_with?("api_title:") api_title_exists = true elsif lines[index + n].strip.start_with?("api_description:") api_description_exists = true end n += 1 end elsif current_field && line.strip.start_with?("heading:") heading_value = line.split(":").last&.strip indent = " " * current_indentation if heading_value&.start_with?("&", "*") new_content << line else heading_value = "&#{current_field} #{heading_value}" new_content << "#{indent}heading: #{heading_value}\n" end field_key = heading_value[1..]&.split(" ")&.first new_content << "#{indent}api_title: *#{field_key}\n" unless api_title_exists new_content << "#{indent}api_description: *#{field_key}\n" unless api_description_exists next end end new_content << line end # Only write to file if there are changes unless lines.join == new_content.join differing_files_count += 1 # Compute the relative path manually relative_path = file.sub(/^#{Regexp.escape(locales_root)}\/?/, "") backup_path = File.join(backup_dir, relative_path) FileUtils.mkdir_p(File.dirname(backup_path)) FileUtils.cp(file, backup_path) puts "↳ Updating, backup created: #{backup_path}" # Write the updated data back to the file File.write(file, new_content.join) end end puts "---" puts "Total files updated: #{differing_files_count}/#{original_files.size}" end end end