diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..9ae23cf --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,56 @@ +--- +version: 2.1 + +jobs: + rubocop: + docker: + - image: 'cimg/base:2021.10' + parameters: + ruby-version: + type: string + steps: + - checkout + - ruby/install: + version: << parameters.ruby-version >> + - ruby/install-deps + - ruby/rubocop-check: + format: progress + label: Inspecting with Rubocop + + test: + docker: + - image: 'cimg/base:2021.10' + parameters: + ruby-version: + type: string + steps: + - checkout + - ruby/install: + version: << parameters.ruby-version >> + - ruby/install-deps + - ruby/rspec-test + - store_artifacts: + path: coverage + +orbs: + ruby: circleci/ruby@1 + +workflows: + code_quality: + jobs: + - rubocop: + matrix: + parameters: + ruby-version: ["2.7", "3.0"] + filters: + branches: + ignore: + - master + - main + test: + jobs: + - test: + matrix: + parameters: + ruby-version: ["2.7", "3.0"] + diff --git a/.gitignore b/.gitignore index d1b9172..7156e74 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /pkg/ /spec/reports/ /tmp/ +/vendor/ # rspec failure tracking .rspec_status diff --git a/.rubocop.yml b/.rubocop.yml index d8891b8..db10d69 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,179 +1,19 @@ -AllCops: - TargetRubyVersion: 2.2 - - -# Indent private/protected/public as deep as method definitions -AccessModifierIndentation: - EnforcedStyle: outdent - SupportedStyles: - - outdent - - indent - -# Align the elements of a hash literal if they span more than one line. -AlignHash: - # Alignment of entries using hash rocket as separator. Valid values are: - # - # key - left alignment of keys - # 'a' => 2 - # 'bb' => 3 - # separator - alignment of hash rockets, keys are right aligned - # 'a' => 2 - # 'bb' => 3 - # table - left alignment of keys, hash rockets, and values - # 'a' => 2 - # 'bb' => 3 - EnforcedHashRocketStyle: key - # Alignment of entries using colon as separator. Valid values are: - # - # key - left alignment of keys - # a: 0 - # bb: 1 - # separator - alignment of colons, keys are right aligned - # a: 0 - # bb: 1 - # table - left alignment of keys and values - # a: 0 - # bb: 1 - EnforcedColonStyle: key - -# Allow safe assignment in conditions. -AssignmentInCondition: - AllowSafeAssignment: true - -BlockNesting: - Max: 3 - -BracesAroundHashParameters: - EnforcedStyle: no_braces - SupportedStyles: - - braces - - no_braces - -# Indentation of `when`. -CaseIndentation: - EnforcedStyle: case - SupportedStyles: - - case - - end - IndentOneStep: false - -# Checks formatting of special comments -CommentAnnotation: - Keywords: - - TODO - - FIXME - - OPTIMIZE - - HACK - - REVIEW - -# Use empty lines between defs. -EmptyLineBetweenDefs: - # If true, this parameter means that single line method definitions don't - # need an empty line between them. - AllowAdjacentOneLineDefs: false - -Encoding: - Enabled: true - -# Align ends correctly. -Lint/EndAlignment: - # The value `keyword` means that `end` should be aligned with the matching - # keyword (if, while, etc.). - # The value `variable` means that in assignments, `end` should be aligned - # with the start of the variable on the left hand side of `=`. In all other - # situations, `end` should still be aligned with the keyword. - EnforcedStyleAlignWith: variable - SupportedStylesAlignWith: - - keyword - - variable +--- +inherit_from: .rubocop_todo.yml -# Checks use of for or each in multiline loops. -For: - EnforcedStyle: each - SupportedStyles: - - for - - each - -HashSyntax: - EnforcedStyle: ruby19 - SupportedStyles: - - ruby19 - - hash_rockets - -LambdaCall: - EnforcedStyle: call - SupportedStyles: - - call - - braces - -MethodDefParentheses: - EnforcedStyle: require_parentheses - SupportedStyles: - - require_parentheses - - require_no_parentheses - -MethodName: - EnforcedStyle: snake_case - SupportedStyles: - - snake_case - - camelCase - -NumericLiterals: - MinDigits: 10 - -# Allow safe assignment in conditions. -ParenthesesAroundCondition: - AllowSafeAssignment: true - -RedundantReturn: - # When true allows code like `return x, y`. - AllowMultipleReturnValues: false - -Semicolon: - # Allow ; to separate several expressions on the same line. - AllowAsExpressionSeparator: false - -TrailingCommaInLiteral: - EnforcedStyleForMultiline: no_comma - SupportedStylesForMultiline: - - comma - - no_comma - -Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: no_comma - SupportedStylesForMultiline: - - comma - - no_comma +AllCops: + TargetRubyVersion: "2.7" + NewCops: enable + Exclude: + - "bin/**/*" + - "vendor/**/*" + - "spec/db/contentful_migrations/*.rb" -# TrivialAccessors doesn't require exact name matches and doesn't allow -# predicated methods by default. -TrivialAccessors: - ExactNameMatch: false - AllowPredicates: false - Whitelist: - - to_ary - - to_a - - to_c - - to_enum - - to_h - - to_hash - - to_i - - to_int - - to_io - - to_open - - to_path - - to_proc - - to_r - - to_regexp - - to_str - - to_s - - to_sym +Style/Documentation: + Enabled: false -VariableName: - EnforcedStyle: snake_case - SupportedStyles: - - snake_case - - camelCase +Metrics/BlockLength: + Exclude: + - '*.gemspec' + - "spec/**/*_spec.rb" -WordArray: - Enabled: False diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..45b2950 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,50 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2021-11-10 17:01:10 UTC using RuboCop version 1.22.3. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +Lint/IneffectiveAccessModifier: + Exclude: + - 'lib/contentful_migrations/migrator.rb' + +# Offense count: 2 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 23 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 110 + +# Offense count: 1 +# Configuration parameters: IgnoredMethods. +Metrics/CyclomaticComplexity: + Max: 11 + +# Offense count: 2 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +Metrics/MethodLength: + Max: 17 + +# Offense count: 2 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + MaxOptionalParameters: 4 + Max: 6 + +# Offense count: 1 +# Configuration parameters: IgnoredMethods. +Metrics/PerceivedComplexity: + Max: 12 + +# Offense count: 1 +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: + Exclude: + - 'lib/contentful_migrations/migration_content_type.rb' diff --git a/Gemfile b/Gemfile index 7ceced1..e8b53d5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ -source "https://rubygems.org" +# frozen_string_literal: true -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } +source 'https://rubygems.org' + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in contentful-migrations.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 13805c5..4e50a98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,68 +1,107 @@ PATH remote: . specs: - contentful-migrations (0.1.4) - contentful-management (~> 2.6) - http (~> 4.1.1) + contentful-migrations (0.2.1) + contentful-management (~> 3.0) GEM remote: https://rubygems.org/ specs: - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - byebug (10.0.2) - coderay (1.1.2) - contentful-management (2.12.0) + ast (2.4.2) + byebug (11.1.3) + climate_control (1.0.1) + coderay (1.1.3) + contentful-management (3.3.0) http (> 1.0, < 5.0) json (>= 1.8, < 3.0) multi_json (~> 1) - diff-lcs (1.3) + diff-lcs (1.4.4) + docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - http (4.1.1) + ffi (1.15.5) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake + http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 2.0) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) + http-form_data (~> 2.2) + http-parser (~> 1.2.0) + http-cookie (1.0.4) domain_name (~> 0.5) http-form_data (2.3.0) - http_parser.rb (0.6.0) - json (2.3.0) - method_source (0.9.2) - multi_json (1.14.1) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - public_suffix (4.0.3) - rake (12.3.2) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + http-parser (1.2.3) + ffi-compiler (>= 1.0, < 2.0) + json (2.6.1) + method_source (1.0.0) + multi_json (1.15.0) + parallel (1.21.0) + parser (3.0.2.0) + ast (~> 2.4.1) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + public_suffix (4.0.6) + rainbow (3.0.0) + rake (13.0.6) + regexp_parser (2.1.1) + rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.3) + rspec_junit_formatter (0.4.1) + rspec-core (>= 2, < 4, != 2.12.0) + rubocop (1.22.3) + parallel (~> 1.10) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.12.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.13.0) + parser (>= 3.0.1.1) + rubocop-rspec (2.6.0) + rubocop (~> 1.19) + ruby-progressbar (1.11.0) + simplecov (0.21.2) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.3) unf (0.1.4) unf_ext - unf_ext (0.0.7.6) + unf_ext (0.0.8) + unicode-display_width (2.1.0) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.16) - byebug (~> 10.0.0) + bundler + byebug + climate_control contentful-migrations! pry - rake (~> 12.3.0) - rspec (~> 3.6) + rspec + rspec_junit_formatter + rubocop-rspec + simplecov BUNDLED WITH - 1.17.3 + 2.2.3 diff --git a/README.md b/README.md index 76ae994..df295b5 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ end ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). diff --git a/Rakefile b/Rakefile index 535c7bd..82bb534 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,8 @@ -require "bundler/gem_tasks" -require "rspec/core/rake_task" +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task :default => :spec - # "./lib/tasks/downloader.rake" -import './lib/tasks/contentful_migrations.rake' +task default: :spec diff --git a/bin/console b/bin/console index b797d0c..5941b53 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require "bundler/setup" -require "contentful_migrations" +require 'bundler/setup' +require 'contentful_migrations' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. @@ -10,5 +11,5 @@ require "contentful_migrations" # require "pry" # Pry.start -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/contentful-migrations.gemspec b/contentful-migrations.gemspec index 3e44cb6..6a4f00f 100644 --- a/contentful-migrations.gemspec +++ b/contentful-migrations.gemspec @@ -1,5 +1,6 @@ +# frozen_string_literal: true -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'contentful_migrations/version' @@ -12,29 +13,33 @@ Gem::Specification.new do |spec| spec.summary = 'Contentful Migrations in Ruby' spec.description = 'Migration library system for Contentful API dependent on contentful-management gem and plagarized from activerecord.' - spec.homepage = "https://github.com/monkseal/contentful-migrations.rb" + spec.homepage = 'https://github.com/monkseal/contentful-migrations.rb' spec.license = 'MIT' + spec.required_ruby_version = '>= 2.7.0' + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # to allow pushing to a single host or delete this section to allow pushing to any host. if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = "https://rubygems.org" + spec.metadata['allowed_push_host'] = 'https://rubygems.org' else raise 'RubyGems 2.0 or newer is required to protect against ' \ - 'public gem pushes.' + 'public gem pushes.' end - spec.files = Dir["{lib,vendor}/**/*"] - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.files = Dir['lib/**/*'] + spec.bindir = 'bin' + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + spec.add_dependency 'contentful-management', '~> 3.0' - spec.add_dependency 'contentful-management', '~> 2.6' - spec.add_dependency 'http', '~> 4.1.1' - + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'byebug' + spec.add_development_dependency 'climate_control' spec.add_development_dependency 'pry' - spec.add_development_dependency 'bundler', '~> 1.16' - spec.add_development_dependency 'rake', '~> 12.3.0' - spec.add_development_dependency 'rspec', '~> 3.6' - spec.add_development_dependency 'byebug', '~> 10.0.0' + spec.add_development_dependency 'rspec' + spec.add_development_dependency 'rspec_junit_formatter' + spec.add_development_dependency 'rubocop-rspec' + spec.add_development_dependency 'simplecov' + spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/contentful/migrations.rb b/lib/contentful/migrations.rb index 0be713c..d3948e8 100644 --- a/lib/contentful/migrations.rb +++ b/lib/contentful/migrations.rb @@ -1 +1,3 @@ -require File.expand_path('../../contentful_migrations', __FILE__) +# frozen_string_literal: true + +require File.expand_path('../contentful_migrations', __dir__) diff --git a/lib/contentful_migrations.rb b/lib/contentful_migrations.rb index f83186e..65a7b90 100644 --- a/lib/contentful_migrations.rb +++ b/lib/contentful_migrations.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'contentful/management' -require 'contentful_migrations/utils' require 'contentful_migrations/version' require 'contentful_migrations/migration_content_type' require 'contentful_migrations/migration_proxy' require 'contentful_migrations/migration' require 'contentful_migrations/migrator' -load 'tasks/contentful_migrations.rake' if defined?(Rails) +require 'contentful_migrations/railtie' if defined?(Rails::Railtie) diff --git a/lib/contentful_migrations/migration.rb b/lib/contentful_migrations/migration.rb index 2ae6445..31d60da 100644 --- a/lib/contentful_migrations/migration.rb +++ b/lib/contentful_migrations/migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ContentfulMigrations class Migration attr_reader :name, :version, :contentful_client, :contentful_space @@ -34,6 +36,7 @@ def record_migration(migration_content_type) def erase_migration(migration_content_type) entry = migration_content_type.entries.all.find { |m| m.version.to_i == version.to_i } return unless entry + entry.unpublish entry.destroy entry diff --git a/lib/contentful_migrations/migration_content_type.rb b/lib/contentful_migrations/migration_content_type.rb index d9bfd19..f31bb43 100644 --- a/lib/contentful_migrations/migration_content_type.rb +++ b/lib/contentful_migrations/migration_content_type.rb @@ -1,45 +1,47 @@ +# frozen_string_literal: true + +require 'contentful/management' + module ContentfulMigrations class MigrationContentType - DEFAULT_MIGRATION_CONTENT_TYPE = 'migrations'.freeze - - attr_reader :access_token, :space_id, :client, :space, - :migration_content_type_name, :logger - - def initialize(client:, - space:, - logger:, - migration_content_type_name: DEFAULT_MIGRATION_CONTENT_TYPE) - @client = client - @space = space - @logger = logger - @migration_content_type_name = migration_content_type_name + attr_reader :environment, :content_type_name + + def initialize(environment:, content_type_name:) + @environment = environment + @content_type_name = content_type_name end - def resolve - @migration_content_type ||= find_or_create_migration_content_type + def content_type + @content_type ||= find_or_create_content_type end - private + private - def find_or_create_migration_content_type - content_type = space.content_types.find(migration_content_type_name) - if content_type.nil? || content_type.is_a?(Contentful::Management::Error) - build_migration_content_type - else - content_type - end + def content_types + @content_types ||= environment.content_types end - def build_migration_content_type - content_type = space.content_types.create( - name: migration_content_type_name, - id: migration_content_type_name, + def find_or_create_content_type + content_types.find(content_type_name) + + # This would be better if it were the proper + # Contentful::Management::NotFound error, but they're difficult to + # recreate in testing. + rescue StandardError + create_content_type + end + + def create_content_type + ct = content_types.create( + name: content_type_name, + id: content_type_name, description: 'Migration Table for interal use only, do not delete' ) - content_type.fields.create(id: 'version', name: 'version', type: 'Integer') - content_type.save - content_type.publish - content_type + ct.fields.create(id: 'version', name: 'version', type: 'Integer') + ct.save + ct.publish + + ct end end end diff --git a/lib/contentful_migrations/migration_proxy.rb b/lib/contentful_migrations/migration_proxy.rb index a9d4e69..f2bdfdd 100644 --- a/lib/contentful_migrations/migration_proxy.rb +++ b/lib/contentful_migrations/migration_proxy.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'forwardable' -require 'contentful_migrations/utils' +require 'contentful_migrations/string_refinements' module ContentfulMigrations # MigrationProxy is used to defer loading of the actual migration classes @@ -8,7 +10,7 @@ module ContentfulMigrations MigrationProxy = Struct.new(:name, :version, :filename, :scope) do extend Forwardable - include Utils + using StringRefinements def initialize(name, version, filename, scope) super @@ -29,7 +31,7 @@ def migration def load_migration require(File.expand_path(filename)) - constantize(name).new(name, version) - end + name.constantize.new(name, version) + end end end diff --git a/lib/contentful_migrations/migrator.rb b/lib/contentful_migrations/migrator.rb index 08f2f7e..ae3440e 100644 --- a/lib/contentful_migrations/migrator.rb +++ b/lib/contentful_migrations/migrator.rb @@ -1,28 +1,45 @@ +# frozen_string_literal: true + +require 'contentful/management/client' +require 'contentful_migrations/string_refinements' + module ContentfulMigrations - class Migrator - include Utils + class Migrator # rubocop:disable Metrics/ClassLength + using StringRefinements - class InvalidMigrationPath < StandardError #:nodoc: + class InvalidMigrationPath < StandardError # :nodoc: def initialize(migrations_path) super("#{migrations_path} is not a valid directory.") end end - DEFAULT_MIGRATION_PATH = 'db/contentful_migrations'.freeze + DEFAULT_MIGRATION_PATH = 'db/contentful_migrations' + DEFAULT_MIGRATION_CONTENT_TYPE_NAME = 'migrations' def self.migrate(args = {}) - new(parse_options(args)).migrate + new(**parse_options(args)).migrate end def self.rollback(args = {}) - new(parse_options(args)).rollback + new(**parse_options(args)).rollback end def self.pending(args = {}) - new(parse_options(args)).pending + new(**parse_options(args)).pending end - attr_reader :migrations_path, :access_token, :space_id, :client, :space, :env_id, + def self.parse_options(args) + { + migrations_path: ENV.fetch('MIGRATION_PATH', DEFAULT_MIGRATION_PATH), + access_token: ENV.fetch('CONTENTFUL_MANAGEMENT_ACCESS_TOKEN'), + space_id: ENV.fetch('CONTENTFUL_SPACE_ID'), + migration_content_type_name: ENV.fetch('CONTENTFUL_MIGRATION_CONTENT_TYPE', + DEFAULT_MIGRATION_CONTENT_TYPE_NAME), + logger: Logger.new($stdout) + }.merge(args) + end + + attr_reader :migrations_path, :access_token, :space_id, :env_id, :migration_content_type_name, :logger, :page_size def initialize(migrations_path:, @@ -36,22 +53,34 @@ def initialize(migrations_path:, @logger = logger @space_id = space_id @migration_content_type_name = migration_content_type_name - @client = Contentful::Management::Client.new(access_token) @env_id = env_id || ENV['CONTENTFUL_ENV'] || 'master' - @space = @client.environments(space_id).find(@env_id) @page_size = 1000 validate_options end + def client + @client ||= Contentful::Management::Client.new(access_token, raise_errors: true) + end + + def environment + return @environment if @environment.is_a?(Contentful::Management::Environment) + + env = client.environments(space_id).find(env_id) + + # Set the default locale on the environment's client (ugh) + default_locale = env.locales.all.find(&:default) + env.client.configuration[:default_locale] = default_locale.code + + @environment = env + end + def migrate runnable = migrations(migrations_path).reject { |m| ran?(m) } - if runnable.empty? - logger.info('No migrations to run, everything up to date!') - end + logger.info('No migrations to run, everything up to date!') if runnable.empty? runnable.each do |migration| logger.info("running migration #{migration.version} #{migration.name} ") - migration.migrate(:up, client, space) + migration.migrate(:up, client, environment) migration.record_migration(migration_content_type) end self @@ -61,7 +90,7 @@ def rollback already_migrated = migrations(migrations_path).select { |m| ran?(m) } migration = already_migrated.pop logger.info("Rolling back migration #{migration.version} #{migration.name} ") - migration.migrate(:down, client, space) + migration.migrate(:down, client, environment) migration.erase_migration(migration_content_type) end @@ -73,17 +102,7 @@ def pending end end - private - - def self.parse_options(args) - { - migrations_path: ENV.fetch('MIGRATION_PATH', DEFAULT_MIGRATION_PATH), - access_token: ENV['CONTENTFUL_MANAGEMENT_ACCESS_TOKEN'], - space_id: ENV['CONTENTFUL_SPACE_ID'], - migration_content_type_name: MigrationContentType::DEFAULT_MIGRATION_CONTENT_TYPE, - logger: Logger.new(STDOUT) - }.merge(args) - end + private def validate_options raise InvalidMigrationPath, migrations_path unless File.directory?(migrations_path) @@ -120,7 +139,7 @@ def migrations(paths) paths = Array(paths) migrations = migration_files(paths).map do |file| version, name, scope = parse_migration_filename(file) - ContentfulMigrations::MigrationProxy.new(camelize(name), version.to_i, file, scope) + ContentfulMigrations::MigrationProxy.new(name.camelize, version.to_i, file, scope) end migrations.sort_by(&:version) @@ -132,11 +151,11 @@ def migration_files(paths) def migration_content_type @migration_content_type ||= MigrationContentType.new( - space: space, client: client, logger: logger - ).resolve + environment: environment, content_type_name: migration_content_type_name + ).content_type end - MIGRATION_FILENAME_REGEX = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ + MIGRATION_FILENAME_REGEX = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/.freeze def parse_migration_filename(filename) File.basename(filename).scan(MIGRATION_FILENAME_REGEX).first diff --git a/lib/contentful_migrations/railtie.rb b/lib/contentful_migrations/railtie.rb new file mode 100644 index 0000000..d2f22cf --- /dev/null +++ b/lib/contentful_migrations/railtie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ContentfulMiagrations + class Railtie < Rails::Railtie + rake_tasks do + load File.join(File.dirname(__FILE__), '..', 'tasks', 'contentful_migrations.rake') + end + + generators do + require_relative '../generators/contentful_migration/contentful_migration_generator' + end + end +end diff --git a/lib/contentful_migrations/string_refinements.rb b/lib/contentful_migrations/string_refinements.rb new file mode 100644 index 0000000..1e5dd29 --- /dev/null +++ b/lib/contentful_migrations/string_refinements.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module ContentfulMigrations + module StringRefinements + refine String do + # This method was taken from ActiveSupport::Inflector to avoid having + # a dependency on ActiveSupport in this project. + # http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize + def camelize(uppercase_first_letter: true) + string = if uppercase_first_letter + sub(/^[a-z\d]*/, &:capitalize) + else + sub(/^((?=\b|[A-Z_])|\w)/, &:downcase) + end + string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" } + string.gsub!('/', '::') + string + end + + # This method was taken from ActiveSupport::Inflector to avoid having + # a dependency on ActiveSupport in this project. + # http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize + def constantize + names = split('::') + + # Trigger a built-in NameError exception including the ill-formed constant in the message. + Object.const_get(self) if names.empty? + + # Remove the first blank element in case of '::ClassName' notation. + names.shift if names.size > 1 && names.first.empty? + + names.inject(Object) do |constant, name| + if constant == Object + constant.const_get(name) + else + candidate = constant.const_get(name) + next candidate if constant.const_defined?(name, false) + next candidate unless Object.const_defined?(name) + + # Go down the ancestors to check if it is owned directly. The check + # stops when we reach Object or the end of ancestors tree. + constant = constant.ancestors.each_with_object(constant) do |ancestor, const| + break const if ancestor == Object + break ancestor if ancestor.const_defined?(name, false) + end + + # owner is in Object, so raise + constant.const_get(name, false) + end + end + end + end + end +end diff --git a/lib/contentful_migrations/utils.rb b/lib/contentful_migrations/utils.rb deleted file mode 100644 index ddc521f..0000000 --- a/lib/contentful_migrations/utils.rb +++ /dev/null @@ -1,51 +0,0 @@ -module ContentfulMigrations - module Utils - # This method was taken from ActiveSupport::Inflector to avoid having - # a dependency on ActiveSupport in this project. - # http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize - def camelize(term, uppercase_first_letter = true) - string = term.to_s - string = if uppercase_first_letter - string.sub(/^[a-z\d]*/, &:capitalize) - else - string.sub(/^((?=\b|[A-Z_])|\w)/, &:downcase) - end - string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" } - string.gsub!('/'.freeze, '::'.freeze) - string - end - # This method was taken from ActiveSupport::Inflector to avoid having - # a dependency on ActiveSupport in this project. - # http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize - - def constantize(camel_cased_word) - names = camel_cased_word.split('::'.freeze) - - # Trigger a built-in NameError exception including the ill-formed constant in the message. - Object.const_get(camel_cased_word) if names.empty? - - # Remove the first blank element in case of '::ClassName' notation. - names.shift if names.size > 1 && names.first.empty? - - names.inject(Object) do |constant, name| - if constant == Object - constant.const_get(name) - else - candidate = constant.const_get(name) - next candidate if constant.const_defined?(name, false) - next candidate unless Object.const_defined?(name) - - # Go down the ancestors to check if it is owned directly. The check - # stops when we reach Object or the end of ancestors tree. - constant = constant.ancestors.each_with_object(constant) do |ancestor, const| - break const if ancestor == Object - break ancestor if ancestor.const_defined?(name, false) - end - - # owner is in Object, so raise - constant.const_get(name, false) - end - end - end - end -end diff --git a/lib/contentful_migrations/version.rb b/lib/contentful_migrations/version.rb index 39175c9..e78e662 100644 --- a/lib/contentful_migrations/version.rb +++ b/lib/contentful_migrations/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ContentfulMigrations - VERSION = '0.1.4'.freeze + VERSION = '0.2.1' end diff --git a/lib/generators/contentful_migration/contentful_migration_generator.rb b/lib/generators/contentful_migration/contentful_migration_generator.rb index 579955f..283cb67 100644 --- a/lib/generators/contentful_migration/contentful_migration_generator.rb +++ b/lib/generators/contentful_migration/contentful_migration_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators' class ContentfulMigrationGenerator < Rails::Generators::NamedBase @@ -22,7 +24,7 @@ def down end end - FILE + FILE end def next_migration_number diff --git a/lib/tasks/contentful_migrations.rake b/lib/tasks/contentful_migrations.rake index 57f653c..0211137 100644 --- a/lib/tasks/contentful_migrations.rake +++ b/lib/tasks/contentful_migrations.rake @@ -1,17 +1,19 @@ +# frozen_string_literal: true + require 'contentful_migrations' namespace :contentful_migrations do desc 'Migrate the contentful space, runs all pending migrations' - task :migrate, [:contentful_space] do |_t, _args| + task migrate: :environment do |_t, _args| ContentfulMigrations::Migrator.migrate end desc 'Rollback previous contentful migration' - task :rollback, [:contentful_space] do |_t, _args| + task rollback: :environment do |_t, _args| ContentfulMigrations::Migrator.rollback end desc 'List any pending contentful migrations' - task :pending, [:contentful_space] do |_t, _args| + task pending: :environment do |_t, _args| ContentfulMigrations::Migrator.pending end end diff --git a/spec/db/contentful_migrations/20180216021826_build_test_content.rb b/spec/db/contentful_migrations/20180216021826_build_test_content.rb index 89770a4..b42831b 100644 --- a/spec/db/contentful_migrations/20180216021826_build_test_content.rb +++ b/spec/db/contentful_migrations/20180216021826_build_test_content.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BuildTestContent < ContentfulMigrations::Migration def up with_space do |space| diff --git a/spec/lib/contentful_migrations/migration_content_type_spec.rb b/spec/lib/contentful_migrations/migration_content_type_spec.rb index 154f4f6..95aadd7 100644 --- a/spec/lib/contentful_migrations/migration_content_type_spec.rb +++ b/spec/lib/contentful_migrations/migration_content_type_spec.rb @@ -1,63 +1,50 @@ +# frozen_string_literal: true + +require 'byebug' +require 'contentful_migrations/migration_content_type' + RSpec.describe ContentfulMigrations::MigrationContentType do - let(:client) { double(:client) } - let(:space) { double(:space) } - - let(:logger) { double(:logger) } - let(:defaults) do - { - client: client, - space: space, - logger: logger - } - end + subject { described_class.new(**defaults) } - describe '#initialize' do - it 'sets name and version' do - migrator = described_class.new(defaults) + let(:environment) { instance_double(Contentful::Management::Environment) } + let(:content_type_name) { 'foos' } + let(:defaults) { { environment: environment, content_type_name: content_type_name } } - expect(migrator.client).to eq(client) - expect(migrator.space).to eq(space) - expect(migrator.migration_content_type_name).to eq('migrations') - end - end + describe '#content_type' do + subject { described_class.new(**defaults).content_type } - describe '#resolve' do - subject { described_class.new(defaults) } + let(:content_types) { double(Contentful::Management::ContentType) } + let(:content_type) { instance_double(Contentful::Management::ContentType) } + + before do + expect(environment).to receive(:content_types).and_return(content_types) + end - let(:content_types) { double(:content_types) } - let(:migration_content_type) { double(:migration_content_type) } context 'when content type exists' do before do - expect(space).to receive(:content_types).and_return(content_types) - expect(content_types).to receive(:find).with('migrations').and_return(migration_content_type) + expect(content_types).to receive(:find).with(content_type_name).and_return(content_type) end - it 'calls contentful to retrive content type' do - expect(subject.resolve).to eq(migration_content_type) - end + it { is_expected.to eq content_type } end context 'when content type not exist' do - let(:fields) { double(:fields) } + let(:fields) { double(Contentful::Management::Field) } + before do - allow(space).to receive(:content_types).and_return(content_types) - expect(content_types).to receive(:find).with('migrations').and_return(nil) + expect(content_types).to receive(:find).with(content_type_name).and_raise(StandardError) expect(content_types).to receive(:create).with( - name: 'migrations', - id: 'migrations', + name: content_type_name, + id: content_type_name, description: 'Migration Table for interal use only, do not delete' - ).and_return(migration_content_type) - expect(migration_content_type).to receive(:fields).and_return(fields) - expect(fields).to receive(:create).with( - id: 'version', name: 'version', type: 'Integer' - ) - expect(migration_content_type).to receive(:save) - expect(migration_content_type).to receive(:publish) + ).and_return(content_type) + expect(content_type).to receive(:fields).and_return(fields) + expect(fields).to receive(:create).with(id: 'version', name: 'version', type: 'Integer') + expect(content_type).to receive(:save) + expect(content_type).to receive(:publish) end - it 'calls contentful to retrive content type' do - expect(subject.resolve).to eq(migration_content_type) - end + it { is_expected.to eq content_type } end end end diff --git a/spec/lib/contentful_migrations/migration_proxy_spec.rb b/spec/lib/contentful_migrations/migration_proxy_spec.rb index 05ee319..2bd3d76 100644 --- a/spec/lib/contentful_migrations/migration_proxy_spec.rb +++ b/spec/lib/contentful_migrations/migration_proxy_spec.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'contentful_migrations/migration_proxy' + RSpec.describe ContentfulMigrations::MigrationProxy do let(:version) { '20180216021826' } let(:filename) { 'spec/db/contentful_migrations/20180216021826_build_test_content.rb' } diff --git a/spec/lib/contentful_migrations/migration_spec.rb b/spec/lib/contentful_migrations/migration_spec.rb index 8d3f702..a876ac3 100644 --- a/spec/lib/contentful_migrations/migration_spec.rb +++ b/spec/lib/contentful_migrations/migration_spec.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'contentful_migrations/migration' + RSpec.describe ContentfulMigrations::Migration do let(:version) { '20180216021826' } let(:name) { 'BuildTestContent' } diff --git a/spec/lib/contentful_migrations/migrator_spec.rb b/spec/lib/contentful_migrations/migrator_spec.rb index a7d51f1..bb08a29 100644 --- a/spec/lib/contentful_migrations/migrator_spec.rb +++ b/spec/lib/contentful_migrations/migrator_spec.rb @@ -1,133 +1,170 @@ -RSpec.describe ContentfulMigrations::Migrator do - ######## - ## Class methods - ######## +# frozen_string_literal: true - describe '.migrate' do - let(:migrated) { double(:migrated) } +require 'climate_control' +require 'contentful_migrations/migrator' +require 'contentful_migrations/migration_proxy' +require 'contentful_migrations/migration_content_type' - before do - expect(described_class).to receive(:new).and_return(double(:m, migrate: migrated)) +RSpec.describe ContentfulMigrations::Migrator do + describe 'class methods' do + let(:env) do + { + CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: management_access_token, + CONTENTFUL_SPACE_ID: space_id + } end + let(:management_access_token) { 'management_access_token' } + let(:space_id) { 'space_id' } - it 'calls migrate' do - expect(described_class.migrate).to eq(migrated) + around do |example| + ClimateControl.modify(env) do + example.run + end end - end - describe '.rollback' do - let(:rolledback) { double(:rolledback) } + describe '.migrate' do + let(:migrated) { double(:migrated) } - before do - expect(described_class).to receive(:new).and_return(double(:m, rollback: rolledback)) - end + before do + expect(described_class).to receive(:new).and_return(double(:m, migrate: migrated)) + end - it 'calls migrate' do - expect(described_class.rollback).to eq(rolledback) + it 'calls migrate' do + expect(described_class.migrate).to eq(migrated) + end end - end - describe '.pending' do - let(:pending_result) { double(:pending_result) } + describe '.rollback' do + let(:rolledback) { double(:rolledback) } - before do - expect(described_class).to receive(:new).and_return(double(:m, pending: pending_result)) - end + before do + expect(described_class).to receive(:new).and_return(double(:m, rollback: rolledback)) + end - it 'calls migrate' do - expect(described_class.pending).to eq(pending_result) + it 'calls migrate' do + expect(described_class.rollback).to eq(rolledback) + end end - end - ######## - ## Instance methods - ######## - - let(:logger) { double(:logger) } - let(:defaults) do - { migrations_path: 'spec/db/contentful_migrations', - access_token: 'access_token', - space_id: 'space_id', - migration_content_type_name: ContentfulMigrations::MigrationContentType::DEFAULT_MIGRATION_CONTENT_TYPE, - logger: logger, - env_id: 'master' } - end - - describe '#initialize' do - it 'sets name and version' do - migrator = described_class.new(defaults) + describe '.pending' do + let(:pending_result) { double(:pending_result) } - expect(migrator.migrations_path).to eq('spec/db/contentful_migrations') - expect(migrator.access_token).to eq('access_token') - expect(migrator.space_id).to eq('space_id') - expect(migrator.migration_content_type_name).to eq('migrations') - expect(migrator.env_id).to eq('master') - end - it 'raises error when invalid path' do - expect do - described_class.new(defaults.merge(migrations_path: 'bad/path')) - end.to raise_error(ContentfulMigrations::Migrator::InvalidMigrationPath) - end - end - describe '#migrate' do - subject { described_class.new(defaults) } - context 'when no migrations' do before do - allow(subject).to receive(:migrations).and_return([]) - expect(logger).to receive(:info) + expect(described_class).to receive(:new).and_return(double(:m, pending: pending_result)) end - it 'sets name and version' do - expect(subject.migrate).to eq(subject) + it 'calls migrate' do + expect(described_class.pending).to eq(pending_result) end end + end - context 'when migrations' do - let(:client) { double(:client) } - let(:spaces) { double(:spaces) } - let(:space) { double(:space) } - let(:content_types) { double(:content_types) } - let(:migration_content_type) { double(:migration_content_type) } - let(:entries) { double(:entries, all: all) } - let(:all) { [] } - let(:migration) { double(:migration, version: 20_180_216_021_826, name: 'BuildTestContent') } - - before do - expect(Contentful::Management::Client).to receive(:new).and_return(client) - expect(client).to receive(:environments).with('space_id').and_return(space) - expect(space).to receive(:find).with('master').and_return(space) - allow(subject).to receive(:migration_content_type).and_return(migration_content_type) - allow(logger).to receive(:info) - end + describe 'instance methods' do + subject(:migrator) { described_class.new(**defaults) } + + let(:logger) { double(:logger) } + let(:migrations_path) { 'spec/db/contentful_migrations' } + let(:access_token) { 'my_access_token' } + let(:space_id) { 'my_space_id' } + let(:migration_content_type_name) { 'my_migrations' } + let(:env_id) { 'my_env_id' } + + let(:defaults) do + { migrations_path: migrations_path, + access_token: access_token, + space_id: space_id, + migration_content_type_name: migration_content_type_name, + logger: logger, + env_id: env_id } + end + describe '#initialize' do it 'sets name and version' do - expect(migration_content_type).to receive(:entries).and_return(entries) - expect(ContentfulMigrations::MigrationProxy).to receive(:new).with( - 'BuildTestContent', - 20_180_216_021_826, - 'spec/db/contentful_migrations/20180216021826_build_test_content.rb', - '' - ).and_return(migration) - expect(migration).to receive(:migrate).with(:up, client, space) - expect(migration).to receive(:record_migration).with(migration_content_type) - expect(subject.migrate).to eq(subject) + expect(migrator.migrations_path).to eq migrations_path + expect(migrator.access_token).to eq access_token + expect(migrator.space_id).to eq space_id + expect(migrator.migration_content_type_name).to eq migration_content_type_name + expect(migrator.env_id).to eq env_id end - it 'sets @page_size during construction' do - expect(subject.instance_variable_get('@page_size')).to eq(1000) + it 'raises error when invalid path' do + expect do + described_class.new(**defaults.merge(migrations_path: 'bad/path')) + end.to raise_error(ContentfulMigrations::Migrator::InvalidMigrationPath) end + end + + describe '#migrate' do + context 'when no migrations' do + before do + allow(migrator).to receive(:migrations).and_return([]) + expect(logger).to receive(:info) + end - it 'calls fetch_page when loading migrated records' do - allow(subject).to receive(:fetch_page).and_return([]) - expect(subject).to receive(:fetch_page).once - subject.send(:migrated) + it 'sets name and version' do + expect(migrator.migrate).to eq(migrator) + end end - it 'pages through contentful records' do - subject.instance_variable_set('@page_size', 10) - allow(subject).to receive(:fetch_page).and_return((1..10).to_a, (1..9).to_a) - expect(subject).to receive(:fetch_page).twice - subject.send(:load_migrated) + context 'when migrations' do + let(:client) { double(:client) } + let(:spaces) { double(:spaces) } + let(:space) { double(:space) } + let(:locales) { double(:locales) } + let(:all_locales) { [default_locale] } + let(:default_locale) { Struct.new(:code, :default).new('en', true) } + let(:space_client) { double(:space_client) } + let(:space_client_config) { {} } + + let(:content_types) { double(:content_types) } + let(:migration_content_type) { double(:migration_content_type) } + let(:entries) { double(:entries, all: all) } + let(:all) { [] } + let(:migration) { double(:migration, version: 20_180_216_021_826, name: 'BuildTestContent') } + + before do + allow(migrator).to receive(:migration_content_type).and_return(migration_content_type) + allow(logger).to receive(:info) + end + + it 'sets name and version' do + expect(Contentful::Management::Client).to receive(:new).and_return(client) + expect(client).to receive(:environments).with(space_id).and_return(space) + expect(space).to receive(:find).with(env_id).and_return(space) + expect(space).to receive(:locales).and_return(locales) + expect(locales).to receive(:all).and_return(all_locales) + expect(space).to receive(:client).and_return(space_client) + expect(space_client).to receive(:configuration).and_return(space_client_config) + + expect(migration_content_type).to receive(:entries).and_return(entries) + expect(ContentfulMigrations::MigrationProxy).to receive(:new).with( + 'BuildTestContent', + 20_180_216_021_826, + 'spec/db/contentful_migrations/20180216021826_build_test_content.rb', + '' + ).and_return(migration) + expect(migration).to receive(:migrate).with(:up, client, space) + expect(migration).to receive(:record_migration).with(migration_content_type) + expect(migrator.migrate).to eq(migrator) + expect(space_client_config[:default_locale]).to eq default_locale.code + end + + it 'sets @page_size during construction' do + expect(migrator.instance_variable_get('@page_size')).to eq(1000) + end + + it 'calls fetch_page when loading migrated records' do + allow(migrator).to receive(:fetch_page).and_return([]) + expect(migrator).to receive(:fetch_page).once + migrator.send(:migrated) + end + + it 'pages through contentful records' do + migrator.instance_variable_set('@page_size', 10) + allow(migrator).to receive(:fetch_page).and_return((1..10).to_a, (1..9).to_a) + expect(migrator).to receive(:fetch_page).twice + migrator.send(:load_migrated) + end end end end diff --git a/spec/lib/contentful_migrations_spec.rb b/spec/lib/contentful_migrations_spec.rb deleted file mode 100644 index 7ec143c..0000000 --- a/spec/lib/contentful_migrations_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -RSpec.describe ContentfulMigrations do - it 'has a version number' do - expect(ContentfulMigrations::VERSION).not_to be nil - end - - it 'does something useful' do - expect(true).to eq(true) - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 09e1fb8..9bb93a4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,11 @@ -require 'bundler/setup' -require 'contentful_migrations' -require 'pry' +# frozen_string_literal: true + +require 'simplecov' + +SimpleCov.start do + enable_coverage :branch + add_filter %r{^/spec/} +end RSpec.configure do |config| # Enable flags like --only-failures and --next-failure