diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 68337d9..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -engines: - rubocop: - enabled: true - eslint: - enabled: true - csslint: - enabled: true -ratings: - paths: - - "**.rb" - - "**.js" - - "**.jsx" - - "**.css" -exclude_paths: -- test/**/* -# This is a sample .codeclimate.yml configured for Engine analysis on Code -# Climate Platform. For an overview of the Code Climate Platform, see here: -# http://docs.codeclimate.com/article/300-the-codeclimate-platform - -# Under the engines key, you can configure which engines will analyze your repo. -# Each key is an engine name. For each value, you need to specify enabled: true -# to enable the engine as well as any other engines-specific configuration. - -# For more details, see here: -# http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform - -# For a list of all available engines, see here: -# http://docs.codeclimate.com/article/296-engines-available-engines - -engines: -# to turn on an engine, add it here and set enabled to `true` -# to turn off an engine, set enabled to `false` or remove it - rubocop: - enabled: true - golint: - enabled: true - gofmt: - enabled: true - eslint: - enabled: true - csslint: - enabled: true - -# Engines can analyze files and report issues on them, but you can separately -# decide which files will receive ratings based on those issues. This is -# specified by path patterns under the ratings key. - -# For more details see here: -# http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform - -# Note: If the ratings key is not specified, this will result in a 0.0 GPA on your dashboard. - -# ratings: -# paths: -# - app/** -# - lib/** -# - "**.rb" -# - "**.go" - -# You can globally exclude files from being analyzed by any engine using the -# exclude_paths key. - -#exclude_paths: -#- spec/**/* -#- vendor/**/* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d0f3432 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..0b58bf2 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,131 @@ +AllCops: + TargetRubyVersion: 2.3 + Exclude: + - tmp/**/* + - gemfiles/**/* + +#--------------------------------------------- +# Layout +#--------------------------------------------- + +# Hashes do not need padding +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +# Allow 2 space indentation for when inside a case +Layout/CaseIndentation: + Enabled: false + +# Allow empty lines in classes +Layout/EmptyLinesAroundClassBody: + Enabled: false + +# Allow multiple spaces before first argument +Layout/SpaceBeforeFirstArg: + Enabled: false + +# Allow extra spacing, e.g. to align components +Layout/ExtraSpacing: + Enabled: false + +# Usually good, but in some cases not possible +Layout/AlignHash: + Enabled: false + +# Allow an empty line after do / before end +Layout/EmptyLinesAroundBlockBody: + Enabled: false + +# Again, generally a good idea, but it has problems with multiline operations in +# combination with assignments +Layout/MultilineOperationIndentation: + Enabled: false + +# See the corresponding other cops +Layout/EmptyLinesAroundModuleBody: + Enabled: false + +Layout/SpaceInLambdaLiteral: + Enabled: false + +#--------------------------------------------- +# Metrics +#--------------------------------------------- + +# Allow bigger classes +Metrics/ClassLength: + Enabled: false + +Metrics/LineLength: + Max: 120 + + # To make it possible to copy or click on URIs in the code, we allow lines + # containing a URI to be longer than Max. + AllowHeredoc: true + AllowURI: true + +Metrics/BlockLength: + Max: 75 + Exclude: + - spec/**/*.rb + +# Allow longer methods +Metrics/MethodLength: + Enabled: false + +# Allow bigger modules +Metrics/ModuleLength: + Enabled: false + +#--------------------------------------------- +# Naming +#--------------------------------------------- + +Naming/HeredocDelimiterNaming: + Enabled: false + +#--------------------------------------------- +# Style +#--------------------------------------------- + +# Allow fail() for initial exception, raise() for re-raise +# It seems that the cop decision was mainly based on "more people use raise than fail"... +Style/SignalException: + Enabled: false + +# Allow assigning multiple variables in one line. +# This should not be overused, but comes in handy when assigning initializer values to instance variables +Style/ParallelAssignment: + Enabled: false + +# Depending on the situation, it might make more sense to use +# [:symbol1, :symbol2] over %i[symbol1 symbol2], e.g. for multiline aligning reasons. +Style/SymbolArray: + Enabled: false + +# Not all modules have to have top level comments +Style/Documentation: + Enabled: false + +# Allow class variable usage +Style/ClassVars: + Enabled: false + +# Allow block comments +Style/BlockComments: + Enabled: false + +# Allow the use of !! (conversion of nil/object to true/false) +Style/DoubleNegation: + Enabled: false + +# Allow unless/if blocks even for one-liners +Style/IfUnlessModifier: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/MissingRespondToMissing: + Exclude: + - lib/setting_accessors/setting_scaffold.rb diff --git a/.travis.yml b/.travis.yml index 9927325..468418f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,19 +2,23 @@ language: ruby cache: bundler rvm: - - 2.2 - - 2.3.1 + - 2.3.0 + - 2.3.3 + - 2.4.0 + - 2.4.4 + - 2.5.0 + - 2.5.3 -env: - - DISABLE_DATABASE_ENVIRONMENT_CHECK=1 +gemfile: + - gemfiles/rails_4.2.gemfile + - gemfiles/rails_5.0.gemfile + - gemfiles/rails_5.1.gemfile + - gemfiles/rails_5.2.gemfile before_install: 'gem install bundler' script: 'bundle exec rake' notifications: email: - recipients: - - stex@sterex.de on_failure: change on_success: never - diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..b1589c8 --- /dev/null +++ b/Appraisals @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +appraise 'rails-4.2' do + gem 'rails', '~> 4.2.0' +end + +appraise 'rails-5.0' do + gem 'rails', '~> 5.0.0' +end + +appraise 'rails-5.1' do + gem 'rails', '~> 5.1.0' +end + +appraise 'rails-5.2' do + gem 'rails', '~> 5.2.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index d539ca7..84a417d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 1.0.0 + +This is almost a complete refactoring of the gem which also removes some not really needed functionality. +It is the first release fully compatible with Rails 5. + +New minimum versions: + +* Ruby: 2.3 +* Rails: 4.2 + +#### General + +* Removed globally defined settings (`config/settings.yml`) and therefore also global validations and type conversions. + It is still possible to create global Settings, but each developer has to make sure that the assigned + value is valid and correctly typed. +* Type conversion follows AR's type conversions more strictly now. + This means that e.g. assigning `1.0` to a boolean setting will result in `true` instead of `nil` as before. +* Updating an attribute created through `setting_accessor` now leads to the record being touched, + similar to what would happen when a normal database attribute was changed. (#4) +* Setting a new value for an attribute created through `attribute_accessor` will now mark + the record as "dirty", meaning `changed?` returns true. (#9) + This was especially needed for Rails 5 as it only even performs a database operation in this case. + +#### Tests + +* Tests are now written in RSpec +* The dummy Rails application has been removed and replaced with `with_model` +* Appraisal was added to test against multiple Rails versions + +#### API Changes + +* `method_missing` is now part of the SettingScaffold and therefore no longer needed in the actual Setting model. + You should remove `method_missing` from your model. +* Setting.create_or_update was renamed to Setting.set and now uses a keyword argument for `assignable` +* `setting_accessor` no longer supports the `fallback` option. + If a `default` option is given, it is automatically used as fallback. + ## 0.3.0 -* Fixed a bug that caused setting accessors not being initialized when being called as part of a rake task chain (#6) \ No newline at end of file +* Fixed a bug that caused setting accessors not being initialized when being called as part of a rake task chain (#6) diff --git a/Gemfile b/Gemfile index 3b4f662..fb06592 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' # Specify your gem's dependencies in setting_accessors.gemspec diff --git a/README.md b/README.md index 7f2332b..b68edc5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ SettingAccessors [![Gem Version](https://badge.fury.io/rb/setting_accessors.svg)](http://badge.fury.io/rb/setting_accessors) [![Build Status](https://travis-ci.org/Stex/setting_accessors.svg?branch=master)](https://travis-ci.org/Stex/setting_accessors) +[![Maintainability](https://api.codeclimate.com/v1/badges/78becd1d005aab2d1409/maintainability)](https://codeclimate.com/github/Stex/setting_accessors/maintainability) Sometimes it's handy to keep track of various settings or attributes in ActiveRecord instances and persist them through various requests (and sessions). An example would be to store a items-per-page value for multiple tables in the application per user or probably also set default sorting directions. @@ -11,6 +12,20 @@ The only problem is, that it would be necessary to either keep hundreds of colum This gem consists of a global key-value-store, allowing to use both global and record bound settings and ActiveRecord integration to add virtual columns to models without having to change the database layout. +Version Information / Backwards Compatibility +--------------------------------------------- + +The current version (1.x) is only compatible with Ruby >= 2.3 and Rails >= 4.2. +Versions 0.x support older versions of both Ruby and Rails. + +Version 0.x also supported validations and type conversions for globally defined settings +(`Setting.my_setting = 5`). +This was removed in this version, so developers should make sure that the assigned +value has the correct type themselves. + +Validations for class-wise defined `setting_accessor`s can still be realized using +ActiveModel's integrated validators as described below. + Installation ------------ @@ -37,7 +52,6 @@ The gem requires a few additional files to work properly: - A migration to create the settings table - A setting model - An initializer -- A settings config file (config/settings.yml) All of them can be generated using the provided generator: @@ -47,12 +61,15 @@ $ rails g setting_accessors:install MODEL_NAME If no model name is given, the default one (`Setting`) is used. -Usage as a global key-value-store (globally defined and anonymous settings) +Usage as a global key-value-store ----- -In the following, the model name will be assumed as `Setting`, though you may choose its name freely using the provided generator (see above). +In the following, the model name will be assumed as `Setting`, +though you may choose its name freely using the provided generator (see above). -Settings can either be global or assigned to an instance of ActiveRecord::Base. They consist of a name which is unique either in the global scope or within the instance and a value which can be everything that's serializable through YAML. +Settings can either be global or assigned to an instance of ActiveRecord::Base. +They consist of a name which is unique either in the global scope or within the instance +and a value which can be everything that's serializable through YAML. The easiest way to use the settings model is as a global key-value-store without validations and typecasting. @@ -64,7 +81,9 @@ Setting.the_meaning_of_life #=> 42 ``` -If the name contains characters which are not allowed due to ruby naming rules, you can also use the methods`Setting[KEY]` resp. `Setting.get(KEY)` to get a setting's value and `Setting[KEY] = VALUE` resp.`Setting.create_or_update(KEY, VALUE)`. +If the name contains characters which are not allowed due to ruby naming rules, +you can also use the methods`Setting[KEY]` resp. `Setting.get(KEY)` to get a setting's value +and `Setting[KEY] = VALUE` resp.`Setting.set(KEY, VALUE)`. Therefore, the following getter methods are equivalent: @@ -79,54 +98,23 @@ For the corresponding setters: ```ruby Setting.meaning_of_life = 42 Setting[:meaning_of_life] = 42 -Setting.create_or_update(:meaning_of_life, 42) +Setting.set(:meaning_of_life, 42) ``` -### Globally defined settings with types and validations - -As stated above, the initializer will generate a file called `settings.yml` in your application's `config` directory. This file is used to define global settings, an entry consists of: - -- The setting's name -- The setting's type (optional) -- Validations to be performed when the setting is saved (optional) -- A default value (optional) - -An example would be a simple string setting: - -```yaml -a_string: - type: string - default: "I am a string!" - validations: - presence: true -``` - -If a setting is defined with a type, automatic type conversion will happen, similar to what ActiveRecord does: +This makes it also possible to use Ruby's or-equals operator: ```ruby -Setting.a_string = 42 -#=> "42" -``` - -The default value is used by the functions `Setting.get_or_default` and `Setting.create_default_setting` and a fallback option for assigned settings (see below). - -The built-in validations currently support, this might be extended in the future. - -| Base Validation | Options | -|:----------------|:---------------------------| -| `presence` | `allow_nil`, `allow_blank` | -| `numericality` | `only_integer` | -| `boolean` |   | - +Setting.meaning_of_life ||= 42 +Setting[:meaning_of_life] ||= 42 +``` ### Assigned Records -Both globally defined settings and "anonymous" settings can also be assigned to -a instances of `ActiveRecord::Base` without having to define them in the model first. +Global settings can also be assigned to an instance of `ActiveRecord::Base` without +having to define them in the model first. -An example would be the above mentioned saving of "items per page" values for -various views: Let's say we have an events view and want to save how many -rows/items each user would like to display. +An example would be to save "items per page" values for various views: +Let's say we have an events view and want to save how many rows/items each user would like to display. As there might be many views requiring this functionality, it wouldn't make sense to define global settings for each of them. Instead, we would use anonymous @@ -138,122 +126,74 @@ def items_per_page default_value = 30 Setting.get(key, current_user) || default_value end -``` - -ActiveRecord Integration ------------------------- - -The gem adds the method `setting_accessor` to each class which inherits from `ActiveRecord::Base`. - -`setting_accessor` takes a setting name and a set of options to customize the accessor's behaviour: -```ruby -class MyModel < ActiveRecord::Base - setting_accessor :a_string, :fallback => :default +def set_items_per_page + key = [controller_path, action_name].join('_') + Setting.set(key, params[:per_page], assignable: current_user) || default_value end ``` -This automatically adds most of the helper methods to the model instances which are available for database columns: +The following calls are equivalent: ```ruby -my_model = MyModel.new - -#Setter -my_model.a_string = 1234 - -#Getter -my_model.a_string -#=> "1234" - -#Value before type cast -my_model.a_string_before_type_cast -#=> 1234 - -#Old value -my_model.a_string_was -#=> "I am a string!" (fallback value, see below) - -#Check if the value was changed -my_model.a_string_changed? -#=> true +Setting[:meaning_of_life, current_universe] = 42 +Setting.set(:meaning_of_life, 42, assignable: current_universe) ``` -The setting records are only persisted if all settings *and* the main record were valid. - -### Integration of globally defined settings +`[]=` is not an exact alias of `set` here to still support `or-else`: -If a setting with the given name is defined in `config/settings.yml`, validations and type settings are automatically fetched from it, in this case, only the `:fallback` option is allowed. +```ruby +Setting[:meaning_of_life, current_universe] ||= 42 +``` -As of now, this means that custom validations are also not supported for globally defined settings, this may be changed in the future. +ActiveRecord Integration +------------------------ -### Class-wise definition of settings +The gem adds the method `setting_accessor` to each class which inherits from `ActiveRecord::Base`. -If a setting is defined using `setting_accessor` which is not part of `config/settings.yml`, it will only be available in this class. It however may be defined in multiple classes independently. +It basically generates an `attribute_accessor` with the given name, but also makes sure that +the value is persisted when the actual record is saved. It also provides the standard attribute helper +methods ActiveRecord adds for each database column (`ATTRIBUTE_changed?`, `ATTRIBUTE_was`, ...). -Defining a class setting accepts the same options as the config file: +It aims to simulate an actual database column. ```ruby class MyModel < ActiveRecord::Base - setting_accessor :my_setting, :type => :boolean, :default => false + setting_accessor :my_setting, type: :boolean, default: false end ``` ### Options ```ruby -:type => :string | :integer | :boolean | :polymorphic +type: :string | :integer | :boolean | :polymorphic ``` -`:type` defines the setting's data type. If no type is given, `:polymorhpic` -is used automatically. +`:type` defines the setting's data type. If no type is given, `:polymorhpic` is used automatically. For every other type, the setting's value is automatically converted accordingly -upon setting it, mimicking ActiveRecord's behaviour regarding database columns. +upon setting it, mimicking ActiveRecord's type casting based on database types. Please note that `:polymorphic` values are saved as is, meaning that you can store everything that's serializable. -More types will be added in the future, most likely all database types -ActiveRecord can handle as well. - ```ruby -:default => "I am a string!" +default: Object ``` -`:default` sets the setting's default value. It can be retrieved either by -calling `Setting.get_or_default(...)` or defining a `:fallback` on a class setting. - -```ruby -:fallback => :global | :default | Object -``` +`:default` sets the setting's default value that is returned as long no actual setting exists for +that name/assignable. -The `:fallback` option specifies which value should be returned in case the -setting did not receive a value yet: - -- `:global` will try to retrieve a global setting (`Setting.NAME`) with the same - name as the accessor and return `nil` if it isn't defined. -- `:default` will return the default value set up either in the accessor method - or `config/settings.yml` for globally defined settings. -- Every other value will be returned as is - - -```ruby -:validations => {:presence => true} -:validations => {:numericality => {:only_integer => true}} -:validations => {:custom => [lambda {|setting| setting.errors.add(:not_42) if setting.value != 42}]} -:validations => {:custom => [:must_be_42]} -``` +### Validations -There are some built-in validations (see above), but they should be mostly used for globally defined -and globally used settings (`Setting.somethingsomething`). -For model specific settings, you may treat the setting accessors just like normal columns -and define your validations accordingly, e.g. +Attributes created by `setting_accessor` can simply be used with `ActiveModel::Validations` the same way +as other attributes: ```ruby -setting_accessor :my_string, :type => :string -setting_accessor :my_number, :type => :integer +setting_accessor :my_string, type: :string +setting_accessor :my_number, type: :integer -validates :my_string, :presence => true -validates :my_number, :numericality => {:only_integer => true} +validates :my_string, presence: true +validates :my_number, numericality: {only_integer: true} ``` Contributing diff --git a/Rakefile b/Rakefile index 5cb00c4..82bb534 100644 --- a/Rakefile +++ b/Rakefile @@ -1,30 +1,8 @@ -require "bundler/gem_tasks" +# frozen_string_literal: true -begin - require 'bundler/setup' -rescue LoadError - puts 'You must `gem install bundler` and `bundle install` to run rake tasks' -end +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' -require 'rdoc/task' +RSpec::Core::RakeTask.new(:spec) -RDoc::Task.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'ArMailerRevised' - rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -Bundler::GemHelper.install_tasks - -require 'rake/testtask' - -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.libs << 'test' - t.pattern = 'test/**/*_test.rb' - t.verbose = false -end - -task default: :test \ No newline at end of file +task default: :spec diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..25159b4 --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'setting_accessors' + +# 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. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..bba3144 --- /dev/null +++ b/bin/setup @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# frozen_string_literal: true + +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/gemfiles/rails_4.1.gemfile b/gemfiles/rails_4.1.gemfile new file mode 100644 index 0000000..6c79f3c --- /dev/null +++ b/gemfiles/rails_4.1.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rails', '~> 4.1.0' + +gemspec path: '../' diff --git a/gemfiles/rails_4.2.gemfile b/gemfiles/rails_4.2.gemfile new file mode 100644 index 0000000..6977eb0 --- /dev/null +++ b/gemfiles/rails_4.2.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 4.2.0" + +gemspec path: "../" diff --git a/gemfiles/rails_4.2.gemfile.lock b/gemfiles/rails_4.2.gemfile.lock new file mode 100644 index 0000000..3431c43 --- /dev/null +++ b/gemfiles/rails_4.2.gemfile.lock @@ -0,0 +1,162 @@ +PATH + remote: .. + specs: + setting_accessors (1.0.0) + activemodel (>= 4.2, <= 5.2) + activerecord (>= 4.2, <= 5.2) + activesupport (>= 4.2, <= 5.2) + +GEM + remote: https://rubygems.org/ + specs: + actionmailer (4.2.10) + actionpack (= 4.2.10) + actionview (= 4.2.10) + activejob (= 4.2.10) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.10) + actionview (= 4.2.10) + activesupport (= 4.2.10) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.10) + activesupport (= 4.2.10) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.10) + activesupport (= 4.2.10) + globalid (>= 0.3.0) + activemodel (4.2.10) + activesupport (= 4.2.10) + builder (~> 3.1) + activerecord (4.2.10) + activemodel (= 4.2.10) + activesupport (= 4.2.10) + arel (~> 6.0) + activesupport (4.2.10) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + appraisal (2.2.0) + bundler + rake + thor (>= 0.14.0) + arel (6.0.4) + ast (2.4.0) + builder (3.2.3) + concurrent-ruby (1.1.3) + crass (1.0.4) + diff-lcs (1.3) + erubis (2.7.0) + generator_spec (0.9.4) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.1-x86_64-darwin-17) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + mini_mime (1.0.1) + mini_portile2 (2.3.0) + minitest (5.11.3) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) + parallel (1.12.1) + parser (2.5.3.0) + ast (~> 2.4.0) + powerpack (0.1.2) + rack (1.6.11) + rack-test (0.6.3) + rack (>= 1.0) + rails (4.2.10) + actionmailer (= 4.2.10) + actionpack (= 4.2.10) + actionview (= 4.2.10) + activejob (= 4.2.10) + activemodel (= 4.2.10) + activerecord (= 4.2.10) + activesupport (= 4.2.10) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.10) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.9) + activesupport (>= 4.2.0, < 5.0) + nokogiri (~> 1.6) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (4.2.10) + actionpack (= 4.2.10) + activesupport (= 4.2.10) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) + rake (10.5.0) + 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) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unicode-display_width (1.4.0) + with_model (2.1.2) + activerecord (>= 4.2, < 6.0) + +PLATFORMS + ruby + +DEPENDENCIES + appraisal (~> 2.2) + bundler (~> 1.6) + generator_spec (~> 0.9) + rails (~> 4.2.0) + rake (~> 10.4) + rspec (~> 3.8) + rubocop (~> 0.60) + setting_accessors! + sqlite3 (~> 1.3) + with_model (~> 2.1) + +BUNDLED WITH + 1.17.1 diff --git a/gemfiles/rails_5.0.gemfile b/gemfiles/rails_5.0.gemfile new file mode 100644 index 0000000..10f52e7 --- /dev/null +++ b/gemfiles/rails_5.0.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.0.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.0.gemfile.lock b/gemfiles/rails_5.0.gemfile.lock new file mode 100644 index 0000000..f07a6bd --- /dev/null +++ b/gemfiles/rails_5.0.gemfile.lock @@ -0,0 +1,169 @@ +PATH + remote: .. + specs: + setting_accessors (1.0.0) + activemodel (>= 4.2, <= 5.2) + activerecord (>= 4.2, <= 5.2) + activesupport (>= 4.2, <= 5.2) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.0.7) + actionpack (= 5.0.7) + nio4r (>= 1.2, < 3.0) + websocket-driver (~> 0.6.1) + actionmailer (5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.7) + actionview (= 5.0.7) + activesupport (= 5.0.7) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.7) + activesupport (= 5.0.7) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.0.7) + activesupport (= 5.0.7) + globalid (>= 0.3.6) + activemodel (5.0.7) + activesupport (= 5.0.7) + activerecord (5.0.7) + activemodel (= 5.0.7) + activesupport (= 5.0.7) + arel (~> 7.0) + activesupport (5.0.7) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + appraisal (2.2.0) + bundler + rake + thor (>= 0.14.0) + arel (7.1.4) + ast (2.4.0) + builder (3.2.3) + concurrent-ruby (1.1.3) + crass (1.0.4) + diff-lcs (1.3) + erubis (2.7.0) + generator_spec (0.9.4) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + i18n (1.0.1) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.1-x86_64-darwin-17) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + method_source (0.9.2) + mini_mime (1.0.1) + mini_portile2 (2.3.0) + minitest (5.11.3) + nio4r (2.3.1) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) + parallel (1.12.1) + parser (2.5.3.0) + ast (~> 2.4.0) + powerpack (0.1.2) + rack (2.0.6) + rack-test (0.6.3) + rack (>= 1.0) + rails (5.0.7) + actioncable (= 5.0.7) + actionmailer (= 5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) + activemodel (= 5.0.7) + activerecord (= 5.0.7) + activesupport (= 5.0.7) + bundler (>= 1.3.0) + railties (= 5.0.7) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.0.7) + actionpack (= 5.0.7) + activesupport (= 5.0.7) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) + rake (10.5.0) + 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) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unicode-display_width (1.4.0) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + with_model (2.1.2) + activerecord (>= 4.2, < 6.0) + +PLATFORMS + ruby + +DEPENDENCIES + appraisal (~> 2.2) + bundler (~> 1.6) + generator_spec (~> 0.9) + rails (~> 5.0.0) + rake (~> 10.4) + rspec (~> 3.8) + rubocop (~> 0.60) + setting_accessors! + sqlite3 (~> 1.3) + with_model (~> 2.1) + +BUNDLED WITH + 1.17.1 diff --git a/gemfiles/rails_5.1.gemfile b/gemfiles/rails_5.1.gemfile new file mode 100644 index 0000000..6100e83 --- /dev/null +++ b/gemfiles/rails_5.1.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.1.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.1.gemfile.lock b/gemfiles/rails_5.1.gemfile.lock new file mode 100644 index 0000000..4a05162 --- /dev/null +++ b/gemfiles/rails_5.1.gemfile.lock @@ -0,0 +1,169 @@ +PATH + remote: .. + specs: + setting_accessors (1.0.0) + activemodel (>= 4.2, <= 5.2) + activerecord (>= 4.2, <= 5.2) + activesupport (>= 4.2, <= 5.2) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.1.6) + actionpack (= 5.1.6) + nio4r (~> 2.0) + websocket-driver (~> 0.6.1) + actionmailer (5.1.6) + actionpack (= 5.1.6) + actionview (= 5.1.6) + activejob (= 5.1.6) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.1.6) + actionview (= 5.1.6) + activesupport (= 5.1.6) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.6) + activesupport (= 5.1.6) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.1.6) + activesupport (= 5.1.6) + globalid (>= 0.3.6) + activemodel (5.1.6) + activesupport (= 5.1.6) + activerecord (5.1.6) + activemodel (= 5.1.6) + activesupport (= 5.1.6) + arel (~> 8.0) + activesupport (5.1.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + appraisal (2.2.0) + bundler + rake + thor (>= 0.14.0) + arel (8.0.0) + ast (2.4.0) + builder (3.2.3) + concurrent-ruby (1.1.3) + crass (1.0.4) + diff-lcs (1.3) + erubi (1.7.1) + generator_spec (0.9.4) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + i18n (1.1.1) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.1) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + method_source (0.9.2) + mini_mime (1.0.1) + mini_portile2 (2.3.0) + minitest (5.11.3) + nio4r (2.3.1) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) + parallel (1.12.1) + parser (2.5.3.0) + ast (~> 2.4.0) + powerpack (0.1.2) + rack (2.0.6) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.1.6) + actioncable (= 5.1.6) + actionmailer (= 5.1.6) + actionpack (= 5.1.6) + actionview (= 5.1.6) + activejob (= 5.1.6) + activemodel (= 5.1.6) + activerecord (= 5.1.6) + activesupport (= 5.1.6) + bundler (>= 1.3.0) + railties (= 5.1.6) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.1.6) + actionpack (= 5.1.6) + activesupport (= 5.1.6) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) + rake (10.5.0) + 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) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unicode-display_width (1.4.0) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + with_model (2.1.2) + activerecord (>= 4.2, < 6.0) + +PLATFORMS + ruby + +DEPENDENCIES + appraisal (~> 2.2) + bundler (~> 1.6) + generator_spec (~> 0.9) + rails (~> 5.1.0) + rake (~> 10.4) + rspec (~> 3.8) + rubocop (~> 0.60) + setting_accessors! + sqlite3 (~> 1.3) + with_model (~> 2.1) + +BUNDLED WITH + 1.17.1 diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile new file mode 100644 index 0000000..5a706dc --- /dev/null +++ b/gemfiles/rails_5.2.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.2.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile.lock b/gemfiles/rails_5.2.gemfile.lock new file mode 100644 index 0000000..30edaa2 --- /dev/null +++ b/gemfiles/rails_5.2.gemfile.lock @@ -0,0 +1,177 @@ +PATH + remote: .. + specs: + setting_accessors (1.0.0) + activemodel (>= 4.2, <= 5.2) + activerecord (>= 4.2, <= 5.2) + activesupport (>= 4.2, <= 5.2) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.2.0) + actionpack (= 5.2.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.0) + actionpack (= 5.2.0) + actionview (= 5.2.0) + activejob (= 5.2.0) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.2.0) + actionview (= 5.2.0) + activesupport (= 5.2.0) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.0) + activesupport (= 5.2.0) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.2.0) + activesupport (= 5.2.0) + globalid (>= 0.3.6) + activemodel (5.2.0) + activesupport (= 5.2.0) + activerecord (5.2.0) + activemodel (= 5.2.0) + activesupport (= 5.2.0) + arel (>= 9.0) + activestorage (5.2.0) + actionpack (= 5.2.0) + activerecord (= 5.2.0) + marcel (~> 0.3.1) + activesupport (5.2.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + appraisal (2.2.0) + bundler + rake + thor (>= 0.14.0) + arel (9.0.0) + ast (2.4.0) + builder (3.2.3) + concurrent-ruby (1.1.3) + crass (1.0.4) + diff-lcs (1.3) + erubi (1.7.1) + generator_spec (0.9.4) + activesupport (>= 3.0.0) + railties (>= 3.0.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + i18n (1.1.1) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.1) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) + method_source (0.9.2) + mimemagic (0.3.2) + mini_mime (1.0.1) + mini_portile2 (2.3.0) + minitest (5.11.3) + nio4r (2.3.1) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) + parallel (1.12.1) + parser (2.5.3.0) + ast (~> 2.4.0) + powerpack (0.1.2) + rack (2.0.6) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.0) + actioncable (= 5.2.0) + actionmailer (= 5.2.0) + actionpack (= 5.2.0) + actionview (= 5.2.0) + activejob (= 5.2.0) + activemodel (= 5.2.0) + activerecord (= 5.2.0) + activestorage (= 5.2.0) + activesupport (= 5.2.0) + bundler (>= 1.3.0) + railties (= 5.2.0) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.2.0) + actionpack (= 5.2.0) + activesupport (= 5.2.0) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) + rake (10.5.0) + 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) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unicode-display_width (1.4.0) + websocket-driver (0.7.0) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + with_model (2.1.2) + activerecord (>= 4.2, < 6.0) + +PLATFORMS + ruby + +DEPENDENCIES + appraisal (~> 2.2) + bundler (~> 1.6) + generator_spec (~> 0.9) + rails (~> 5.2.0) + rake (~> 10.4) + rspec (~> 3.8) + rubocop (~> 0.60) + setting_accessors! + sqlite3 (~> 1.3) + with_model (~> 2.1) + +BUNDLED WITH + 1.17.1 diff --git a/lib/generators/setting_accessors/install_generator.rb b/lib/generators/setting_accessors/install_generator.rb index d4969b3..884cc1a 100644 --- a/lib/generators/setting_accessors/install_generator.rb +++ b/lib/generators/setting_accessors/install_generator.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + module SettingAccessors module Generators class InstallGenerator < Rails::Generators::Base include Rails::Generators::Migration - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) - argument :model_name, :type => :string, :default => 'Setting' + argument :model_name, type: :string, default: 'Setting' - def self.next_migration_number(path) + def self.next_migration_number(_nbr) if @prev_migration_nr @prev_migration_nr += 1 else @@ -22,12 +24,12 @@ def create_install template 'model.rb.erb', "app/models/#{model_name.classify.underscore}.rb" migration_template 'migration.rb.erb', "db/migrate/create_#{model_name.classify.underscore.pluralize}.rb" - initializer 'setting_accessors.rb', < < ActiveRecord::Base belongs_to :assignable, :polymorphic => true include SettingAccessors::SettingScaffold - - # - # Makes accessing settings a little easier. - # Examples: - # - # #Loading **the value** of a global setting named "my_setting" - # Setting.my_setting - # - # #Setting **the value** of a global setting named "my_setting" - # Setting.my_setting = [1,2,3,4,5] - # - # #Loading **the value** of an assigned setting named "cool_setting" - # #+some_cool_user+ is here an instance of ActiveRecord::Base - # Setting.cool_setting(some_cool_user) - # - def self.method_missing(method, *args) - super(method, *args) if args.size > 1 - method_name = method.to_s - if method_name.last == '=' - self.create_or_update(method_name[0..method_name.length-2], args.first, nil, true) - else - self.get(method_name, args.first) - end - end end \ No newline at end of file diff --git a/lib/setting_accessors.rb b/lib/setting_accessors.rb index 483fea8..cb4a81b 100644 --- a/lib/setting_accessors.rb +++ b/lib/setting_accessors.rb @@ -1,11 +1,20 @@ +# frozen_string_literal: true + +require 'active_record' +require 'active_model/validations' + require 'setting_accessors/version' -require 'setting_accessors/accessor' -require 'setting_accessors/converter' +require 'setting_accessors/helpers' +require 'setting_accessors/accessor_generator' +require 'setting_accessors/converters/base' +require 'setting_accessors/converters/boolean_converter' +require 'setting_accessors/converters/integer_converter' +require 'setting_accessors/converters/string_converter' +require 'setting_accessors/converters/polymorphic_converter' require 'setting_accessors/integration' -require 'setting_accessors/integration_validator' require 'setting_accessors/internal' require 'setting_accessors/setting_scaffold' -require 'setting_accessors/validator' +require 'setting_accessors/setting_set' ActiveRecord::Base.class_eval do include SettingAccessors::Integration @@ -13,7 +22,7 @@ module SettingAccessors def self.setting_class - self.setting_class_name.constantize + setting_class_name.constantize end def self.setting_class=(klass) diff --git a/lib/setting_accessors/accessor.rb b/lib/setting_accessors/accessor.rb deleted file mode 100644 index 5547ab8..0000000 --- a/lib/setting_accessors/accessor.rb +++ /dev/null @@ -1,189 +0,0 @@ -# -# Helper class to make accessing record specific settings easier -# -class SettingAccessors::Accessor - - def initialize(record) - @record = record - @temp_settings = {} - end - - # - # Tries to retrieve the given setting's value from the temp settings - # (already read/written values in this instance). If the setting hasn't been - # used before, its value is retrieved from the database. - # - # If a setting hasn't been read by this record (instance) before, its value - # is stored in the local read set. - # - # TODO: See if this causes problems with read settings not being updated by external changes. - # User1: Read Setting X - # User2: Update Setting X - # User1: Read Setting X -> Gets old value from temp settings. - # This shouldn't be too dangerous as the system state will be refreshed with every request though. - # - def [](key) - return @temp_settings[key.to_sym] if has_key?(key) - value = SettingAccessors.setting_class.get(key, @record) - @temp_settings[key.to_sym] = value unless value.nil? - value - end - - # - # Tries to fetch a setting value using the provided key and #[]. - # It will only return the +default+ value if there is - # - no temporary setting with the given key AND - # - no already persisted setting (see #[]) - # - def fetch(key, default = nil) - result = self[key] - return default if result.nil? && !has_key?(key) - result - end - - # - # Like #fetch, but it will store the default value as a temporary setting - # if no actual setting value could be found. This is useful to further work - # with default setting values. - # The default value is cloned (using #dup to avoid copying object states) before - # it is assigned. This will not work for singleton instances like true, false, etc. - # - def fetch_and_store(key, default = nil) - result = self[key] - if result.nil? && !has_key?(key) - self[key] = default.duplicable? ? default.dup : default - else - result - end - end - - def has_key?(key) - @temp_settings.has_key?(key.to_sym) - end - - # - # Writes a setting's value - # - def []=(key, val) - set_value_before_type_cast(key, val) - @temp_settings[key.to_sym] = SettingAccessors::Internal.converter(value_type(key)).convert(val) - end - - # - # Tries to find a setting for this record. - # If none is found, will return the default setting value - # specified in the setting config file. - # - def get_or_default(key) - fetch_and_store(key, SettingAccessors.setting_class.get_or_default(key, @record)) - end - - # - # Tries to find a setting for this record first. - # If none is found, tries to find a global setting with the same name - # - def get_or_global(key) - fetch_and_store(key, SettingAccessors.setting_class.get(key)) - end - - # - # Tries to find a setting for this record first, - # if none is found, it will return the given value instead. - # - def get_or_value(key, value) - fetch_and_store(key, value) - end - - def get_with_fallback(key, fallback = nil) - return self[key] if fallback.nil? - - case fallback.to_s - when 'default' then get_or_default(key) - when 'global' then get_or_global(key) - else get_or_value(key, fallback) - end - end - - # - # @return [String] the setting's value type in the +@record+'s context - # - def value_type(key) - SettingAccessors::Internal.setting_value_type(key, @record) - end - - #---------------------------------------------------------------- - # ActiveRecord Helper Methods Emulation - #---------------------------------------------------------------- - - def value_was(key, fallback = nil) - return SettingAccessors.setting_class.get(key, @record) if fallback.nil? - - case fallback.to_s - when 'default' then SettingAccessors.setting_class.get_or_default(key, @record) - when 'global' then SettingAccessors.setting_class.get(key) - else fallback - end - end - - def value_changed?(key) - self[key] != value_was(key) - end - - def value_before_type_cast(key) - SettingAccessors::Internal.lookup_nested_hash(@values_before_type_casts, key.to_s) || self[key] - end - - protected - - # - # Keeps a record of the originally set value for a setting before it was - # automatically converted. - # - def set_value_before_type_cast(key, value) - @values_before_type_casts ||= {} - @values_before_type_casts[key.to_s] = value - end - - # - # Validates the new setting values. - # If there is an accessor for the setting, the errors will be - # directly forwarded to it, otherwise to :base - # - # Please do not call this method directly, use the IntegrationValidator - # class instead, e.g. - # - # validates_with SettingAccessors::IntegrationValidator - # - def validate! - @temp_settings.each do |key, value| - validation_errors = SettingAccessors.setting_class.validation_errors(key, value, @record) - validation_errors.each do |message| - if @record.respond_to?("#{key}=") - @record.errors.add(key, message) - else - @record.errors.add :base, :invalid_setting, :name => key, :message => message - end - end - end - end - - # - # Saves the new setting values into the database - # Please note that there is no check if the values changed their - # in the meantime. - # - # Also, this method expects that the settings were validated - # before using #validate! and will therefore not perform - # validations itself. - # - def persist! - @temp_settings.each do |key, value| - Setting.create_or_update(key, value, @record) - end - flush! - end - - def flush! - @temp_settings = {} - end -end diff --git a/lib/setting_accessors/accessor_generator.rb b/lib/setting_accessors/accessor_generator.rb new file mode 100644 index 0000000..364ab5b --- /dev/null +++ b/lib/setting_accessors/accessor_generator.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# +# This class handles creating everything necessary for a new setting_accessor +# and the actual method assignment in the calling class. +# +module SettingAccessors + class AccessorGenerator + attr_reader :setting_name + attr_reader :options + + def initialize(setting_name, **options) + @setting_name = setting_name + @options = options + end + + def assign_setting!(klass) + SettingAccessors::Internal.set_class_setting(klass, setting_name, options) + define_getters(klass) + define_setter(klass) + define_active_record_helpers(klass) + end + + private + + def define_getters(klass, outer_setting_name = setting_name) + setting_type = SettingAccessors::Internal.setting_value_type(setting_name, klass).to_sym + + klass.class_eval do + define_method(outer_setting_name) do + settings.get_or_default(outer_setting_name) + end + + # Getter alias for boolean settings + alias_method "#{outer_setting_name}?", outer_setting_name if setting_type == :boolean + end + end + + def define_setter(klass, outer_setting_name = setting_name) + klass.class_eval do + define_method("#{outer_setting_name}=") do |new_value| + settings[outer_setting_name] = new_value + end + end + end + + def define_active_record_helpers(klass, outer_setting_name = setting_name) + klass.class_eval do + # NAME_was + define_method("#{outer_setting_name}_was") do + settings.value_was(outer_setting_name) + end + + # NAME_before_type_cast + define_method("#{outer_setting_name}_before_type_cast") do + settings.value_before_type_cast(outer_setting_name) + end + + # NAME_changed? + define_method("#{outer_setting_name}_changed?") do + settings.value_changed?(outer_setting_name) + end + end + end + end +end diff --git a/lib/setting_accessors/converter.rb b/lib/setting_accessors/converter.rb deleted file mode 100644 index 1987397..0000000 --- a/lib/setting_accessors/converter.rb +++ /dev/null @@ -1,71 +0,0 @@ -# -# This class hopefully will hopefully one day mimic ActiveRecord's -# attribute assigning methods, meaning that a conversion to the column type -# is done as soon as a new value is assigned by the programmer. -# -# If the value cannot be parsed in the required type, +nil+ is assigned. -# Please make sure that you specify the correct validations in settings.yml -# or assigned model to avoid this. -# -# Currently supported types: -# - Fixnum -# - String -# - Boolean -# -# If the type is 'polymorphic', it is not converted at all. -# -class SettingAccessors::Converter - - def initialize(value_type) - @value_type = value_type - end - - # - # Converts the setting's value to the correct type - # - def convert(new_value) - #If the value is set to be polymorphic, we don't have to convert anything. - return new_value if @value_type.to_s == 'polymorphic' - - #ActiveRecord only converts non-nil values to their database type - #during assignment - return new_value if new_value.nil? - - parse_method = :"parse_#{@value_type}" - - if private_methods.include?(parse_method) - send(parse_method, new_value) - else - Rails.logger.warn("Invalid Setting type: #{@value_type}") - new_value - end - end - - private - - def parse_boolean(value) - case value - when TrueClass, FalseClass - value - when String - return true if %w[true 1].include?(value.downcase) - return false if %w[false 0].include?(value.downcase) - nil - when Fixnum - return true if value == 1 - return false if value.zero? - nil - else - nil - end - end - - def parse_integer(value) - value.to_i - end - - def parse_string(value) - value.to_s - end - -end diff --git a/lib/setting_accessors/converters/base.rb b/lib/setting_accessors/converters/base.rb new file mode 100644 index 0000000..0750989 --- /dev/null +++ b/lib/setting_accessors/converters/base.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module SettingAccessors + module Converters + class Base + attr_reader :value + + def initialize(value) + @value = value + end + + def convert + # ActiveRecord does not convert +nil+ values to the corresponding database type + return value if value.nil? + + parse_value + end + + def self.parse_value + raise NotImplementedError + end + end + end +end diff --git a/lib/setting_accessors/converters/boolean_converter.rb b/lib/setting_accessors/converters/boolean_converter.rb new file mode 100644 index 0000000..52560ad --- /dev/null +++ b/lib/setting_accessors/converters/boolean_converter.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module SettingAccessors + module Converters + class BooleanConverter < Base + def parse_value + case value + when TrueClass, FalseClass + value + when String + parse_string + when Integer + parse_integer + else + parse_other_value + end + end + + private + + # + # Special handler for Rails 4.2, see the following deprecation warning: + # + # DEPRECATION WARNING: You attempted to assign a value which is not explicitly `true` or `false` (0.0) + # to a boolean column. Currently this value casts to `false`. + # This will change to match Ruby's semantics, and will cast to `true` in Rails 5. + # If you would like to maintain the current behavior, you should explicitly handle + # the values you would like cast to `false`. + # + def parse_other_value + Gem.loaded_specs['activerecord'].version >= Gem::Version.create('5.0') + end + + def parse_integer + return true if value.to_i == 1 + return false if value.to_i.zero? + + nil + end + + def parse_string + case value.downcase + when 'true', '1' then true + when 'false', '0' then false + when '' then nil + else parse_other_value + end + end + end + end +end diff --git a/lib/setting_accessors/converters/integer_converter.rb b/lib/setting_accessors/converters/integer_converter.rb new file mode 100644 index 0000000..80fc086 --- /dev/null +++ b/lib/setting_accessors/converters/integer_converter.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module SettingAccessors + module Converters + class IntegerConverter < Base + def parse_value + return parse_boolean if value == true || value == false + + value.to_i + rescue NoMethodError + nil + end + + private + + def parse_boolean + value ? 1 : 0 + end + end + end +end diff --git a/lib/setting_accessors/converters/polymorphic_converter.rb b/lib/setting_accessors/converters/polymorphic_converter.rb new file mode 100644 index 0000000..3bafe6a --- /dev/null +++ b/lib/setting_accessors/converters/polymorphic_converter.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module SettingAccessors + module Converters + class PolymorphicConverter < Base + def parse_value + value + end + end + end +end diff --git a/lib/setting_accessors/converters/string_converter.rb b/lib/setting_accessors/converters/string_converter.rb new file mode 100644 index 0000000..4ac756d --- /dev/null +++ b/lib/setting_accessors/converters/string_converter.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module SettingAccessors + module Converters + class StringConverter < Base + def parse_value + value.to_s + end + end + end +end diff --git a/lib/setting_accessors/helpers.rb b/lib/setting_accessors/helpers.rb new file mode 100644 index 0000000..1e8fd39 --- /dev/null +++ b/lib/setting_accessors/helpers.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module SettingAccessors + module Helpers + class Error < StandardError; end + class NestedHashKeyNotFoundException < Error; end + + def ensure_nested_hash!(hash, *keys) + h = hash + keys.each do |key| + h[key] ||= {} + h = h[key] + end + end + + def lookup_nested_hash(hash, *keys) + fail NestedHashKeyNotFoundException if hash.nil? + + h = hash + keys.each do |key| + fail NestedHashKeyNotFoundException unless h.key?(key) + + h = h[key] + end + h + end + end +end diff --git a/lib/setting_accessors/integration.rb b/lib/setting_accessors/integration.rb index d33b27d..b95d1aa 100644 --- a/lib/setting_accessors/integration.rb +++ b/lib/setting_accessors/integration.rb @@ -1,117 +1,103 @@ -module SettingAccessors::Integration - def self.included(base) - base.validates_with SettingAccessors::IntegrationValidator +# frozen_string_literal: true + +module SettingAccessors + module Integration + def self.included(base) + # After the main record was saved, we can save its settings. + # This is necessary as the record might have been a new record + # without an ID yet + base.after_save do + settings.send(:persist!) + + # From AR 5.1 on, #_update actually checks whether the changed "attributes" are actually + # table columns or not. If no actual changed columns were found, the record is not changed and + # only the after_* callbacks are executed. + # This means that the settings are persisted, but the record's +updated_at+ column is not updated. + # + # This workaround triggers a #touch on the record in case no actual column change already + # triggered a timestamp update. + # + # TODO: This might lead to #after_commit being triggered twice, once by #update_* and once by #touch + touch if @_setting_accessors_touch_assignable + @_setting_accessors_touch_assignable = false + end - # After the main record was saved, we can save its settings. - # This is necessary as the record might have been a new record - # without an ID yet - base.after_save do - settings.send(:persist!) + base.extend ClassMethods end - base.extend ClassMethods - end - - module ClassMethods + module ClassMethods + + # + # Generates a new accessor (=getter and setter) for the given setting + # + # @param [String, Symbol] setting_name + # The setting's name + # + # @param [Hash] options + # Options to customize the behaviour of the generated accessor + # + def setting_accessor(setting_name, **options) + generator = AccessorGenerator.new(setting_name, **options) + generator.assign_setting!(self) + end + end # - # Generates a new accessor (=getter and setter) for the given setting - # - # @param [String, Symbol] setting_name - # The setting's name + # Previously read setting values have to be refreshed if a record is reloaded. + # Without this, #reload'ing a record would not update its setting values to the + # latest database version if they were previously read. # - # @param [Hash] options - # Options to customize the behaviour of the generated accessor + # Example to demonstrate the problem with this override: + # user = User.create(:a_boolean => true) + # user_alias = User.find(user.id) + # user.a_boolean = !user_alias.a_boolean + # user.save + # user_alias.reload + # user_alias.a_boolean + # #=> true # - # @option options [Symbol, Object] :fallback (nil) - # If set to +:default+, the getter will return the setting's default - # value if no own value was specified for this record + def reload(*) + super.tap { @settings = nil } + end + # - # If set to +:global+, the getter will try to find a global - # setting if no record specific setting was found + # Adds changed settings to ActiveModel's list of changed attributes. + # This is necessary for #changed? to work correctly without actually overriding + # the method itself. # - # If set to another value, this value is used by default + # TODO: Check if it makes more sense to hook into AR5's AttributeMutationTracker instead # - # If not set at all or to +nil+, the getter will only search for a record specific - # setting and return +nil+ if none was specified previously. + # @return [Hash] All changed attributes # - def setting_accessor(setting_name, options = {}) - fallback = options.delete(:fallback) - - SettingAccessors::Internal.set_class_setting(self, setting_name, options) - - setting_type = SettingAccessors::Internal.setting_value_type(setting_name, self).to_sym - - #Add the setting's name to the list of setting_accessors for this class - SettingAccessors::Internal.add_setting_accessor_name(self, setting_name) - - # Getter - define_method(setting_name) do - settings.get_with_fallback(setting_name, fallback) - end - - # Getter alias for boolean settings - alias_method "#{setting_name}?", setting_name if setting_type == :boolean - - # Setter - define_method("#{setting_name}=") do |new_value| - settings[setting_name] = new_value - end - - #NAME_was - define_method("#{setting_name}_was") do - settings.value_was(setting_name, fallback) - end - - #NAME_before_type_cast - define_method("#{setting_name}_before_type_cast") do - settings.value_before_type_cast(setting_name) - end + def changed_attributes + super.merge(settings.changed_settings) + end - #NAME_changed? - define_method("#{setting_name}_changed?") do - settings.value_changed?(setting_name) + # + # Marks the record to be #touch'd after updating it in case no actual + # attributes were involved and only settings were changed. + # This is necessary for ActiveRecord >= 5.1 which will not perform the + # timestamp updates if it thinks nothing actually changed. + # + if Gem.loaded_specs['activerecord'].version >= Gem::Version.create('5.1') + def _update_record(*) + super.tap do |affected_rows| + # Workaround to trigger a #touch if necessary, see +after_save+ callback further up + @_setting_accessors_touch_assignable = affected_rows.zero? + end end end - end - - # - # Previously read setting values have to be refreshed if a record is reloaded. - # Without this, #reload'ing a record would not update its setting values to the - # latest database version if they were previously read. - # - # Example to demonstrate the problem with this override: - # user = User.create(:a_boolean => true) - # user_alias = User.find(user.id) - # user.a_boolean = !user_alias.a_boolean - # user.save - # user_alias.reload - # user_alias.a_boolean - # #=> true - # - def reload(*) - super - @settings_accessor = nil - self - end - - def as_json(options = {}) - json = super options - setting_names = SettingAccessors::Internal.setting_accessor_names(self.class) - if only = options[:only] - setting_names &= Array(only).map(&:to_s) - elsif except = options[:except] - setting_names -= Array(except).map(&:to_s) + def as_json(options = {}) + super.tap do |json| + SettingAccessors::Internal.json_setting_names(self.class, **options).each do |setting_name| + json[setting_name.to_s] = send(setting_name) + end + end end - setting_names.each do |setting_name| - json[setting_name.to_s] = send(setting_name) + def settings + @settings ||= SettingAccessors::SettingSet.new(self) end - json - end - - def settings - @settings_accessor ||= SettingAccessors::Accessor.new(self) end end diff --git a/lib/setting_accessors/integration_validator.rb b/lib/setting_accessors/integration_validator.rb deleted file mode 100644 index f031ab0..0000000 --- a/lib/setting_accessors/integration_validator.rb +++ /dev/null @@ -1,15 +0,0 @@ -# -# This class handles model validations for assigned records, e.g. -# if the settings are accessed using the Accessor class in this module. -# Only the new temp values are validated using the setting config. -# -# The main work is still done in the Accessor class, so we don't have -# to access its instance variables here, this class acts as a wrapper -# for Rails' validation chain -# - -class SettingAccessors::IntegrationValidator < ActiveModel::Validator - def validate(record) - record.settings.send(:validate!) - end -end \ No newline at end of file diff --git a/lib/setting_accessors/internal.rb b/lib/setting_accessors/internal.rb index 0caeac8..ff5ad1a 100644 --- a/lib/setting_accessors/internal.rb +++ b/lib/setting_accessors/internal.rb @@ -1,48 +1,14 @@ +# frozen_string_literal: true + # # This module contains class methods used internally. # module SettingAccessors module Internal + extend Helpers - def self.ensure_nested_hash!(hash, *keys) - h = hash - keys.each do |key| - h[key] ||= {} - h = h[key] - end - end - - def self.lookup_nested_hash(hash, *keys) - return nil if hash.nil? - - h = hash - keys.each do |key| - return nil if h[key].nil? - h = h[key] - end - h - end - - # - # Loads information about all settings from YAML file - # These are cached in the class so they don't have to be reloaded - # every time. - # - # Note: For development / test, this is flushed every time - # - def self.global_config - if Rails.env.test? || Rails.env.development? - (YAML.load(File.open(Rails.root.join('config/settings.yml'))) || {}).deep_stringify_keys - else - @@config ||= (YAML.load(File.open(Rails.root.join('config/settings.yml'))) || {}).deep_stringify_keys - end - end - - # - # @return [TrueClass, FalseClass] +true+ if the setting is defined in config/settings.yml - # - def self.globally_defined_setting?(setting_name) - self.global_config[setting_name.to_s].present? + def self.class_settings + @@class_settings ||= {} end # @@ -52,20 +18,14 @@ def self.globally_defined_setting?(setting_name) # by using setting_accessor in your model class # def self.set_class_setting(klass, setting_name, options = {}) - @@class_settings ||= {} - - #If there are no options given, the setting *has* to be defined globally. - if options.empty? && !self.globally_defined_setting?(setting_name) - raise ArgumentError.new "The setting '#{setting_name}' in model '#{klass.to_s}' is neither globally defined nor did it receive options" - - #A setting which is already globally defined, may not be re-defined on class base - elsif self.globally_defined_setting?(setting_name) && options.any? - raise ArgumentError.new("The setting #{setting_name} is already defined in config/settings.yml and may not be redefined in #{klass}") - - #If the setting is defined on class base, we have to store its options - elsif options.any? && !self.globally_defined_setting?(setting_name) - self.ensure_nested_hash!(@@class_settings, klass.to_s) - @@class_settings[klass.to_s][setting_name.to_s] = options.deep_stringify_keys + # If there are no options given, the setting *has* to be defined globally. + if options.empty? + raise ArgumentError, "The setting '#{setting_name}' in model '#{klass}' is lacking options." + # If the setting is defined on class base, we have to store its options + else + ensure_nested_hash!(class_settings, klass.to_s) + class_settings[klass.to_s][setting_name.to_s] = options.deep_stringify_keys + add_setting_accessor_name(klass, setting_name) end end @@ -80,32 +40,30 @@ def self.setting_data(setting_name, assignable_class = nil) # As a convenience function (and to keep the existing code working), # it is possible to provide a class or an instance of said class assignable_class &&= assignable_class.class unless assignable_class.is_a?(Class) - - (assignable_class && self.get_class_setting(assignable_class, setting_name)) || - self.global_config[setting_name.to_s] || - {} + (assignable_class && get_class_setting(assignable_class, setting_name)) || {} end # # @return [String] the given setting's value type # def self.setting_value_type(*args) - self.setting_data(*args)['type'] || 'polymorphic' + setting_data(*args)['type'] || 'polymorphic' end # # @return [SettingAccessors::Converter] A value converter for the given type # def self.converter(value_type) - @@converters ||= {} - @@converters[value_type.to_sym] ||= SettingAccessors::Converter.new(value_type) + Converters.const_get("#{value_type.to_s.camelize}Converter") end # - # @return [Hash, NilClass] Information about a class specific setting or +nil+ if it wasn't set before + # @return [Hash, nil] Information about a class specific setting or +nil+ if it wasn't set before # def self.get_class_setting(klass, setting_name) - self.lookup_nested_hash(@@class_settings, klass.to_s, setting_name.to_s) + lookup_nested_hash(class_settings, klass.to_s, setting_name.to_s) + rescue ::SettingAccessors::Helpers::NestedHashKeyNotFoundException + nil end # @@ -124,8 +82,23 @@ def self.add_setting_accessor_name(klass, setting_name) # def self.setting_accessor_names(klass) @@setting_accessor_names ||= {} - self.lookup_nested_hash(@@setting_accessor_names, klass.to_s) || [] + lookup_nested_hash(@@setting_accessor_names, klass.to_s) || [] end + # + # Mainly a helper for #as_json calls. + # Evaluates the given options and determines the setting names to be returned. + # + def self.json_setting_names(klass, **options) + setting_names = setting_accessor_names(klass) + + if options[:only] + setting_names & Array(options[:only]).map(&:to_s) + elsif options[:except] + setting_names - Array(options[:except]).map(&:to_s) + else + setting_names + end + end end -end \ No newline at end of file +end diff --git a/lib/setting_accessors/setting_scaffold.rb b/lib/setting_accessors/setting_scaffold.rb index 140fd55..a5a8ddb 100644 --- a/lib/setting_accessors/setting_scaffold.rb +++ b/lib/setting_accessors/setting_scaffold.rb @@ -1,252 +1,185 @@ +# frozen_string_literal: true + # # Helper methods for the chosen setting model # They are in this module to leave the end developer some room for # his own methods in the setting model for his own methods. # -module SettingAccessors::SettingScaffold - - def self.included(base) - base.extend ClassMethods - base.validates_with SettingAccessors::Validator - base.serialize :value +module SettingAccessors + module SettingScaffold - base.validates :name, - :uniqueness => {:scope => [:assignable_type, :assignable_id]}, - :presence => true - end + def self.included(base) + base.extend ClassMethods + base.serialize :value - module ClassMethods - # - # Searches for a setting in the database and returns its value - # - # @param [String, Symbol] name - # The setting's name - # - # @param [ActiveRecord::Base] assignable - # If given, the setting searched has to be assigned to the given record - # If not given, a global setting is searched - # - # @return [Object, NilClass] - # If a setting is found, **its value** is returned. - # If not, +nil+ is returned. - # - def [](name, assignable = nil) - self.setting_record(name, assignable).try(:value) + base.validates :name, + uniqueness: {scope: [:assignable_type, :assignable_id]}, + presence: true end - alias_method :get, :[] + module ClassMethods + # + # Searches for a setting in the database and returns its value + # + # @param [String, Symbol] name + # The setting's name + # + # @param [ActiveRecord::Base] assignable + # If given, the setting searched has to be assigned to the given record + # If not given, a global setting is searched + # + # @return [Object, NilClass] + # If a setting is found, **its value** is returned. + # If not, +nil+ is returned. + # + def get(name, assignable = nil) + setting_record(name, assignable).try(:value) + end - # - # Tries to look the setting up using #get, if no existing setting is found, - # the setting's default value is returned. - # - def get_or_default(name, assignable = nil) - if (val = self[name, assignable]).nil? - self.new(:name => name, :assignable => assignable).default_value - else - val + alias [] get + + # + # Tries to look the setting up using #get, if no existing setting is found, + # the setting's default value is returned. + # + # This only works for class-wise settings, meaning that an assignable has to be present. + # + def get_or_default(name, assignable) + if (val = get(name, assignable)).nil? + new(name: name, assignable: assignable).default_value + else + val + end end - end - # - # Creates or updates the setting with the given name - # - # @param [String, Symbol] name - # The setting's name - # - # @param [Object] value - # The new setting value - # - # @param [ActiveRecord::Base] assignable - # The optional record this setting belongs to. If not - # given, the setting is global. - # - # @param [Boolean] return_value - # If set to +true+, only the setting's value is returned - # - # @return [Object, Setting] - # Depending on +return_value+ either the newly created Setting record - # or the newly assigned value. - # This is due to the fact that Setting.my_setting = 'something' should - # show the same behaviour as other attribute assigns in the system while - # you might still want to get validation errors on custom settings. - # - # Please note that - if +return_value+ is set to +true+, - # #save! is used instead of #save to ensure that validation - # errors are noticed by the programmer / user. - # As this is usually only the case when coming from method_missing, - # it should not happen anyways - # - # @toto: Bless the rains down in Africa! - # - def create_or_update(name, value, assignable = nil, return_value = false) - setting = self.setting_record(name, assignable) - setting ||= self.new(:name => name, :assignable => assignable) - setting.set_value(value) - - if return_value - setting.save! - setting.value - else - setting.save - setting + # + # Creates or updates the setting with the given name + # + # @param [String, Symbol] name + # The setting's name + # + # @param [Object] value + # The new setting value + # + # @param [ActiveRecord::Base] assignable + # The optional record this setting belongs to. If not + # given, the setting is global. + # + # @param [Boolean] return_value + # If set to +true+, only the setting's value is returned + # + # @return [Object] The newly set value + # + # @toto: Bless the rains down in Africa! + # + def set(name, value, assignable: nil) + (setting_record(name, assignable) || new(name: name, assignable: assignable)).tap do |setting| + setting.raw_value = value + setting.save + end.value end - end - # - # Shortcut for #create_or_update - # - # @param [String, Symbol] name - # The setting's name - # - # The second argument is an optional assignable - # - def []=(name, *args) - assignable = args.size > 1 ? args.first : nil - self.create_or_update(name, args.last, assignable, true) + # + # An alias for #set with a slightly different API. + # This allows the following usage: + # Setting['my_setting', my_assignable] ||= new_value + # + def []=(name, *args) + assignable = args.size > 1 ? args.first : nil + set(name, args.last, assignable: assignable) + end + + # + # @return [Object] the default value for the given setting + # + def get_default_value(name, assignable = nil) + new(name: name, assignable: assignable).default_value + end + + # + # Looks up a setting record for the given name and assignable + # Unlike the other methods here, this one actually returns a Setting object + # instead of its value. + # + # @return [Setting, NilClass] The found setting or nil if not existing + # + def setting_record(name, assignable = nil) + find_by(name: name.to_s, assignable: assignable) + end + + # + # Makes accessing settings a little easier. + # Examples: + # + # #Loading **the value** of a global setting named "my_setting" + # Setting.my_setting + # + # #Setting **the value** of a global setting named "my_setting" + # Setting.my_setting = [1,2,3,4,5] + # + # #Loading **the value** of an assigned setting named "cool_setting" + # #+some_cool_user+ is here an instance of ActiveRecord::Base + # Setting.cool_setting(some_cool_user) + # + def method_missing(method, *args, &block) + method_name = method.to_s + + if method_name.last == '=' + return set(method_name[0..-2], args.first) + elsif args.size <= 1 + return get(method_name, args.first) + end + + super + end end # - # Creates a new setting for the given name and assignable, - # using the setting's default value stored in the config file - # - # If the setting already exists, its value will not be overridden - # - # @param [String] name - # The setting's name - # - # @param [ActiveRecord::Base] assignable - # An optional assignable + # @return [Object] the default value for the current setting # - # @example Create a global default setting 'meaning_of_life' - # Setting.create_default_setting(:meaning_of_life) - # - # @example Create a default setting for all users in the system - # User.all.each { |u| Setting.create_default_setting(:some_setting, u) } - # - def create_default_setting(name, assignable = nil) - self.create_or_update(name, self.get_or_default(name, assignable), assignable) + def default_value + data['default'].freeze end # - # @return [Object] the default value for the given setting + # @return [String] the setting's type as specified in settings.yml + # If the setting wasn't specified, a polymorphic type is assumed # - def get_default_value(name, assignable = nil) - self.new(:name => name, :assignable => assignable).default_value + def value_type + data['type'] || 'polymorphic' end # - # Looks up a setting record for the given name and assignable - # Unlike the other methods here, this one actually returns a Setting object - # instead of its value. + # @return [Object] the setting's value before it was type casted using the defined rule in settings.yml + # See #value_before_type_cast for ActiveRecord attributes # - # @return [Setting, NilClass] The found setting or nil if not existing + # We can't use the name #value_before_type_cast here as it would + # shadow ActiveRecord's default one - which might still be needed. # - def setting_record(name, assignable = nil) - self.find_by(:name => name.to_s, :assignable => assignable) + def raw_value + @raw_value || value end # - # Tests, if the given value would be valid for the given - # setting name. This is done in this class method due to - # the process of setting creation through assigned records - # which does not allow going the "normal" way of testing whether - # a setting was saved correctly or not. + # Sets the new setting value by converting the raw value automatically. # - # @return [Array] The validation errors for the setting's value - # - def validation_errors(name, value, assignable = nil) - s = self.new(:name => name, :value => value, :assignable => assignable) - s.valid? - s.errors[:value] || [] + def raw_value=(new_value) + @raw_value = new_value + self.value = converter.new(new_value).convert end - end - - # - # @return [String] the localized setting name - # they are stored in config/locales/settings.LOCALE.yml - # - def localized_name - i18n_lookup(:name) - end - - # - # @return [String] the localized setting description - # see #localized_name - # - def localized_description - i18n_lookup(:description) - end - - # - # Performs an I18n lookup in the settings locale. - # Class based settings are store in 'settings.CLASS.NAME', globally defined settings - # in 'settings.global.NAME' - # - def i18n_lookup(key, options = {}) - options[:scope] = [:settings, :global, self.name] - options[:scope] = [:settings, self.assignable.class.to_s.underscore, self.name] unless SettingAccessors::Internal.globally_defined_setting?(self.name) - I18n.t(key, options) - end - - # - # @return [Object] the default value for the current setting - # - def default_value - data['default'].freeze - end - # - # @return [String] the setting's type as specified in settings.yml - # If the setting wasn't specified, a polymorphic type is assumed - # - def value_type - data['type'] || 'polymorphic' - end - - # - # @return [Object] the setting's value before it was type casted using the defined rule in settings.yml - # See #value_before_type_cast for ActiveRecord attributes - # - # We can't use the name #value_before_type_cast here as it would - # shadow ActiveRecord's default one - which might still be needed. - # - def original_value - @original_value || self.value - end - - # - # Sets the setting's value to the given one - # Performs automatic type casts - # - def set_value(new_value) - @original_value = new_value - self.value = converter.convert(new_value) - end - - private - - def converter - @converter ||= SettingAccessors::Internal.converter(value_type) - end + private - def value_required? - !!validations['required'] - end - - # - # Accessor to the validations part of the setting's data - # - def validations - data['validates'] || {} - end + def converter + @converter ||= SettingAccessors::Internal.converter(value_type) + end - # - # @see {SettingAccessors::Internal#setting_data} for more information - # - def data - SettingAccessors::Internal.setting_data(name, assignable) + # + # @see {SettingAccessors::Internal#setting_data} for more information + # + def data + SettingAccessors::Internal.setting_data(name, assignable) + end end - end diff --git a/lib/setting_accessors/setting_set.rb b/lib/setting_accessors/setting_set.rb new file mode 100644 index 0000000..4d7dfb5 --- /dev/null +++ b/lib/setting_accessors/setting_set.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +# +# Helper class to make accessing record specific settings easier +# +module SettingAccessors + class SettingSet + include ::SettingAccessors::Helpers + + attr_reader :record + + def initialize(record) + @record = record + @temp_settings = {} + end + + # + # Tries to retrieve the given setting's value from the temp settings + # (already read/written values in this instance). If the setting hasn't been + # used before, its value is retrieved from the database. + # + # If a setting hasn't been read by this record (instance) before, its value + # is stored in the local read set. + # + # TODO: See if this causes problems with read settings not being updated by external changes. + # User1: Read Setting X + # User2: Update Setting X + # User1: Read Setting X -> Gets old value from temp settings. + # This shouldn't be too dangerous as the system state will be refreshed with every request though. + # + # @param [Boolean] skip_cached + # If set to +true+, the setting value is freshly loaded from the database, + # even if a value that's not yet persisted exists. + # + def get(key, skip_cached: false) + return @temp_settings[key.to_sym] if !skip_cached && key?(key) + + value = SettingAccessors.setting_class.get(key, record) + @temp_settings[key.to_sym] = value unless value.nil? + value + end + + alias [] get + + def key?(key) + @temp_settings.key?(key.to_sym) + end + + # + # Writes a setting's value + # + def set(key, val) + track_old_value(key) + set_value_before_type_cast(key, val) + @temp_settings[key.to_sym] = SettingAccessors::Internal.converter(value_type(key)).new(val).convert + end + + alias []= set + + # + # Tries to find a setting for this record. + # If none is found, will return the default setting value + # specified in the setting accessor call + # + # @param [Boolean] store_default + # If set to +true+, the setting's default value is written to the temporary settings + # for faster access. Otherwise, a database lookup is performed every time. + # + # @param [Boolean] skip_cached + # If set to +true+, a possible temporary setting is skipped when looking up the value. + # This means, the default value is returned even if there is a not yet persisted change to the setting. + # + def get_or_default(key, store_default: true, skip_cached: false) + result = get(key, skip_cached: skip_cached) + return result if result || (key?(key) && !skip_cached) + + try_dup(SettingAccessors.setting_class.get_or_default(key, record)).tap do |value| + set(key, value) if store_default + end + end + + # + # @return [String] the setting's value type in the +record+'s context + # + def value_type(key) + SettingAccessors::Internal.setting_value_type(key, record) + end + + #---------------------------------------------------------------- + # ActiveRecord Helper Methods Emulation + #---------------------------------------------------------------- + + # + # @return [Object] the value the given setting had after it was last persisted + # + def value_was(key) + lookup_nested_hash(@old_values, key.to_s) + rescue NestedHashKeyNotFoundException + get(key) + end + + def value_changed?(key) + get(key) != value_was(key) + end + + def value_before_type_cast(key) + lookup_nested_hash(@values_before_type_casts, key.to_s) + rescue NestedHashKeyNotFoundException + get(key) + end + + def changed_settings + @temp_settings.select { |k, _| value_changed?(k) } + end + + protected + + # + # Keeps a record of the originally set value for a setting before it was + # automatically converted. + # + def set_value_before_type_cast(key, value) + @values_before_type_casts ||= {} + @values_before_type_casts[key.to_s] = value + end + + # + # Keeps a local copy of a setting's value before it was overridden. + # Once the setting is persisted, this value is cleared. + # + def track_old_value(key) + @old_values ||= {} + + unless @old_values.key?(key.to_s) + @old_values[key.to_s] = get_or_default(key, skip_cached: true, store_default: false) + end + end + + # + # @return [Object] the duplicated value if it is in fact duplicable. The actual value otherwise + # + def try_dup(value) + value.duplicable? ? value.dup : value + end + + # + # Saves the new setting values into the database + # Please note that there is no check if the values changed their + # in the meantime. + # + # Also, this method expects that the settings were validated + # before using #validate! and will therefore not perform + # validations itself. + # + def persist! + @temp_settings.each do |key, value| + Setting.set(key, value, assignable: record) + end + flush! + end + + def flush! + @temp_settings = {} + @values_before_type_casts = {} + @old_values = {} + end + end +end diff --git a/lib/setting_accessors/validator.rb b/lib/setting_accessors/validator.rb deleted file mode 100644 index dee66d7..0000000 --- a/lib/setting_accessors/validator.rb +++ /dev/null @@ -1,144 +0,0 @@ -class SettingAccessors::Validator < ActiveModel::Validator - - def validate(record) - record.send(:validations).each do |key, requirement| - if key.to_s == 'custom' - Array(requirement).each do |validation| - run_custom_validation(record, validation) - end - elsif built_in_validation?(key) - send("validate_#{key}", record, requirement) - else - raise ArgumentError.new("The invalid validation '#{key}' was given in model '#{defining_model(record).to_s}'") - end - end - end - - private - - def defining_model(record) - if SettingAccessors::Internal.globally_defined_setting?(record.name) || !record.assignable - SettingAccessors.setting_class - else - record.assignable.class - end - end - - # - # Runs a custom validation method - # The method may either be a Proc or an instance method in +record+.+class+ - # - def run_custom_validation(record, proc) - case proc - when Proc - proc.call(record) - when Symbol - if defining_model(record).respond_to?(proc) - defining_model(record).send(proc) - else - raise ArgumentError.new "The method '#{proc}' was set up as validation method in model '#{defining_model(record).name}', but doesn't exist." - end - else - raise ArgumentError.new "An invalid validations method was given ('#{proc}')" - end - end - - # - # @return [TrueClass, FalseClass] +true+ if the given validation - # is a built-in one. - # - def built_in_validation?(validation_name) - private_methods.include?("validate_#{validation_name}".to_sym) - end - - # - # Validates that the setting's value is given - # accepts :allow_blank and :allow_nil as options - # - def validate_presence(record, requirement) - return true unless requirement - - if requirement.is_a?(Hash) - if requirement['allow_blank'] && !record.value.nil? || - requirement['allow_nil'] && record.value.nil? || - record.value.present? - true - else - add_error record, :blank - false - end - else - add_error_if record.value.nil? || record.value == '', record, :blank - end - end - - # - # Validates numericality of the setting's value based on the options given - # in settings.yml - # - def validate_numericality(record, options) - #Test if the value is Numeric in any way (float or int) - add_error_if(!parse_value_as_numeric(record.value), record, :not_a_number) && return - - #If the validation was set to check for integer values, do that as well - add_error_if(options['only_integer'] && !parse_value_as_fixnum(record.value), record, :not_an_integer) - end - - # - # Validates whether the given value is a valid boolean - # - def validate_boolean(record, requirement) - add_error_if(requirement && parse_value_as_boolean(record.value).nil?, record, :not_a_boolean) - end - - #---------------------------------------------------------------- - # Helper Methods - #---------------------------------------------------------------- - - def add_error(record, validation, options = {}) - record.errors.add :value, validation, options - end - - def add_error_if(cond, record, validation, options = {}) - add_error(record, validation, options) if cond - cond - end - - # - # Borrowed from Rails' numericality validator - # @return [Float, NilClass] the given String value as a float or nil - # if the value was not a valid Numeric - # - def parse_value_as_numeric(raw_value) - Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ - rescue ArgumentError, TypeError - nil - end - - # - # Borrowed from Rails' numericality validator - # - # @return [Fixnum, NilClass] the given String value as an int or nil - # - def parse_value_as_fixnum(raw_value) - raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ - end - - # - # Tries to parse the given value as a boolean value - # - # @return [TrueClass, FalseClass, NilClass] - # - def parse_value_as_boolean(raw_value) - case raw_value - when TrueClass, FalseClass - raw_value - when String - return true if %w[true 1].include?(raw_value.downcase) - return false if %w[false 0].include?(raw_value.downcase) - nil - else - nil - end - end -end \ No newline at end of file diff --git a/lib/setting_accessors/version.rb b/lib/setting_accessors/version.rb index e71e567..4f5a967 100644 --- a/lib/setting_accessors/version.rb +++ b/lib/setting_accessors/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SettingAccessors - VERSION = '0.3.0'.freeze + VERSION = '1.0.0' end diff --git a/lib/tasks/setting_accessors_tasks.rake b/lib/tasks/setting_accessors_tasks.rake index a9760aa..4be937a 100644 --- a/lib/tasks/setting_accessors_tasks.rake +++ b/lib/tasks/setting_accessors_tasks.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # desc "Explaining what the task does" # task :setting_accessors do # # Task goes here diff --git a/setting_accessors.gemspec b/setting_accessors.gemspec index 114905d..2936d64 100644 --- a/setting_accessors.gemspec +++ b/setting_accessors.gemspec @@ -1,32 +1,40 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'setting_accessors/version' Gem::Specification.new do |spec| spec.name = 'setting_accessors' spec.version = SettingAccessors::VERSION - spec.authors = ["Stefan Exner"] - spec.email = ["stex@sterex.de"] - spec.summary = %q{A global key-value-store and virtual model columns} - spec.description = %q{Adds a global key-value-store to Rails applications and allows adding typed columns - to model classes without having to change the database layout.} + spec.authors = ['Stefan Exner'] + spec.email = ['stex@sterex.de'] + spec.summary = 'A global key-value-store and virtual model columns' + spec.description = 'Adds a global key-value-store to Rails applications and allows adding typed columns + to model classes without having to change the database layout.' spec.homepage = 'https://www.github.com/stex/setting_accessors' - spec.license = "MIT" + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end - spec.files = `git ls-files -z`.split("\x0") - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib"] + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ['lib'] - spec.required_ruby_version = ['>= 2.2.2', '< 3'] + spec.required_ruby_version = ['>= 2.3', '< 3'] - spec.add_development_dependency "bundler", "~> 1.6" - spec.add_development_dependency "rake", "~> 10.4" + spec.add_development_dependency 'appraisal', '~> 2.2' + spec.add_development_dependency 'bundler', '~> 1.6' + spec.add_development_dependency 'generator_spec', '~> 0.9' + spec.add_development_dependency 'rake', '~> 10.4' + spec.add_development_dependency 'rspec', '~> 3.8' + spec.add_development_dependency 'rubocop', '~> 0.60' spec.add_development_dependency 'sqlite3', '~> 1.3' - spec.add_development_dependency 'shoulda', '~> 3.5' - spec.add_development_dependency 'minitest', '~> 5.5' - spec.add_development_dependency 'byebug', '~> 3.5' + spec.add_development_dependency 'with_model', '~> 2.1' - spec.add_dependency 'rails', ['>= 4.1', '< 5.1'] + spec.add_dependency 'activemodel', ['>= 4.2', '<= 5.2'] + spec.add_dependency 'activerecord', ['>= 4.2', '<= 5.2'] + spec.add_dependency 'activesupport', ['>= 4.2', '<= 5.2'] end diff --git a/spec/lib/generators/install_generator_spec.rb b/spec/lib/generators/install_generator_spec.rb new file mode 100644 index 0000000..0e51879 --- /dev/null +++ b/spec/lib/generators/install_generator_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'generator_spec' +require 'generators/setting_accessors/install_generator' + +describe SettingAccessors::Generators::InstallGenerator, type: :generator do + destination File.expand_path('../../../tmp', File.dirname(__FILE__)) + + before(:all) do + prepare_destination + run_generator + end + + it 'creates the default settings.yml config file' do + assert_file 'config/settings.yml' + end + + it 'creates the initializer' do + assert_file 'config/initializers/setting_accessors.rb' + end + + it 'creates the model' do + assert_file 'app/models/setting.rb' + end + + it 'creates the migration' do + assert_migration 'db/migrate/create_settings.rb' + end +end diff --git a/spec/lib/setting_accessors/converters/boolean_converter_spec.rb b/spec/lib/setting_accessors/converters/boolean_converter_spec.rb new file mode 100644 index 0000000..a847150 --- /dev/null +++ b/spec/lib/setting_accessors/converters/boolean_converter_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +describe SettingAccessors::Converters::BooleanConverter do + subject { described_class } + + with_model 'TestModel' do + table do |t| + t.boolean :boolean_attribute + end + end + + it { is_expected.to convert(1).similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert(1.0).similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('true').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('false').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('1').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('0').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert('Oiski').similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert(true).similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert(false).similar_to(TestModel.new).on(:boolean_attribute) } + it { is_expected.to convert(nil).similar_to(TestModel.new).on(:boolean_attribute) } +end diff --git a/spec/lib/setting_accessors/converters/integer_converter_spec.rb b/spec/lib/setting_accessors/converters/integer_converter_spec.rb new file mode 100644 index 0000000..91d6c48 --- /dev/null +++ b/spec/lib/setting_accessors/converters/integer_converter_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +describe SettingAccessors::Converters::IntegerConverter do + subject { described_class } + + with_model 'TestModel' do + table do |t| + t.integer :integer_attribute + end + end + + it { is_expected.to convert(1).similar_to(TestModel.new).on(:integer_attribute) } + it { is_expected.to convert(1.0).similar_to(TestModel.new).on(:integer_attribute) } + it { is_expected.to convert('Oiski').similar_to(TestModel.new).on(:integer_attribute) } + it { is_expected.to convert(true).similar_to(TestModel.new).on(:integer_attribute) } + it { is_expected.to convert(false).similar_to(TestModel.new).on(:integer_attribute) } + it { is_expected.to convert(nil).similar_to(TestModel.new).on(:integer_attribute) } +end diff --git a/spec/lib/setting_accessors/converters/polymorphic_converter_spec.rb b/spec/lib/setting_accessors/converters/polymorphic_converter_spec.rb new file mode 100644 index 0000000..88109e3 --- /dev/null +++ b/spec/lib/setting_accessors/converters/polymorphic_converter_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe SettingAccessors::Converters::PolymorphicConverter do + subject { described_class } + + it { is_expected.to convert(1).to(1) } + it { is_expected.to convert('Oiski').to('Oiski') } + it { is_expected.to convert(true).to(true) } + it { is_expected.to convert(false).to(false) } + it { is_expected.to convert(nil).to(nil) } + it { is_expected.to convert(a: :b).to(a: :b) } + it { is_expected.to convert([1, 2, 3]).to([1, 2, 3]) } +end diff --git a/spec/lib/setting_accessors/converters/string_converter_spec.rb b/spec/lib/setting_accessors/converters/string_converter_spec.rb new file mode 100644 index 0000000..ff16f2c --- /dev/null +++ b/spec/lib/setting_accessors/converters/string_converter_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +describe SettingAccessors::Converters::StringConverter do + subject { described_class } + + with_model 'TestModel' do + table do |t| + t.string :string_attribute + end + end + + it { is_expected.to convert(1).similar_to(TestModel.new).on(:string_attribute) } + it { is_expected.to convert('Oiski').similar_to(TestModel.new).on(:string_attribute) } + it { is_expected.to convert(true).to('true') } + it { is_expected.to convert(false).to('false') } + it { is_expected.to convert(nil).similar_to(TestModel.new).on(:string_attribute) } +end diff --git a/spec/lib/setting_accessors/integration_spec.rb b/spec/lib/setting_accessors/integration_spec.rb new file mode 100644 index 0000000..fa806c9 --- /dev/null +++ b/spec/lib/setting_accessors/integration_spec.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +describe SettingAccessors::Integration, type: :model do + include SettingModel + + #---------------------------------------------------------------- + # #as_json + #---------------------------------------------------------------- + + describe '#as_json' do + with_model 'User' do + table do |t| + t.string :first_name + t.string :last_name + t.timestamps null: false + end + + model do + setting_accessor :polymorphic_setting, type: :polymorphic, default: {}, fallback: :default + setting_accessor :locale, type: :string + setting_accessor :recently_active, type: :boolean, default: true, fallback: :default + + setting_accessor :catchphrase, type: :string, fallback: 'Oiski Poiski!' + setting_accessor :additional_catchphrase, type: :string, fallback: :default, default: 'Kapitanski' + + validates :locale, presence: true + end + end + + let(:user_attributes) { {first_name: 'Sascha', last_name: 'Desman', locale: 'RU-ru'} } + let(:user) { User.create(user_attributes) } + let(:additional_attribute_names) { %w[id created_at updated_at] } + let(:options) { {} } + let(:json) { user.as_json(options) } + let(:settings_and_attributes) { setting_names(User) + user_attributes.keys.map(&:to_s) } + + context 'when being called without options' do + it 'returns all attributes and all settings' do + settings_and_attributes.each do |name| + expect(json).to include(name => user.send(name)) + end + end + end + + context 'when being called with the :only option' do + let(:options) { super().merge(only: %i[first_name locale]) } + + it 'returns only the requested attributes and settings' do + expect(json).to eql user_attributes.slice(:first_name, :locale).stringify_keys + end + end + + context 'when being called with the :except option' do + let(:options) { super().merge(except: %w[first_name recent_activity]) } + + it 'returns all attributes and settings except for the given exclusions' do + (settings_and_attributes - options[:except]).each do |name| + expect(json).to include(name => user.send(name)) + end + + options[:except].each do |name| + expect(json).not_to include(name) + end + end + end + end + + #---------------------------------------------------------------- + # #create_or_update + #---------------------------------------------------------------- + + describe '#create_or_update' do + with_model 'TestModel' do + table do |t| + t.string :string_attribute + t.timestamps null: false + end + + model do + setting_accessor :string_setting, type: :string + end + end + + let!(:record) do + TestModel.create(string_attribute: 'string_value', string_setting: 'string_setting_value', updated_at: 1.day.ago) + end + + shared_examples 'attribute update and touch' do |attribute_name| + it 'persists the record accordingly' do + expect(TestModel.find(record.id).send(attribute_name)).to eql 'Poiski' + end + + it 'updates the +updated_at+ column accordingly' do + expect(TestModel.find(record.id).updated_at).to be_within(5.seconds).of(Time.now) + end + end + + shared_examples 'attribute setter variations' do |attribute_name| + context 'using a normal attribute setter and #save' do + before(:each) do + record.send("#{attribute_name}=", 'Poiski') + record.save! + end + + include_examples 'attribute update and touch', attribute_name + end + + context 'using #update_attribute' do + before(:each) { record.update_attribute(attribute_name, 'Poiski') } + + include_examples 'attribute update and touch', attribute_name + end + + context 'using mass assignment (update_attributes)' do + before(:each) { record.update_attributes(attribute_name => 'Poiski') } + + include_examples 'attribute update and touch', attribute_name + end + end + + context 'when a normal attribute was changed' do + include_examples 'attribute setter variations', :string_attribute + end + + context 'when only a setting was changed' do + include_examples 'attribute setter variations', :string_setting + end + end + + #---------------------------------------------------------------- + # #reload + #---------------------------------------------------------------- + + describe '#reload' do + with_model 'TestModel' do + model do + setting_accessor :a_string, type: :string, default: 'Oiski' + end + end + + it 'refreshes settings from the database' do + record = TestModel.create + expect(record.a_string).to eql 'Oiski' + TestModel.find(record.id).update_attributes(a_string: 'Poiski') + expect { record.reload }.to change(record, :a_string).from('Oiski').to('Poiski') + end + + it 'discards locally changed but not persisted settings' do + record = TestModel.create + record.a_string = 'Poiski' + expect { record.reload }.to change(record, :a_string).from('Poiski').to('Oiski') + end + end + + #---------------------------------------------------------------- + # .setting_accessor + #---------------------------------------------------------------- + + describe '.setting_accessor' do + let(:record) { TestModel.create } + + shared_examples 'getters' do + it 'creates a getter method' do + expect(record).to respond_to(:setting_with_default) + end + + context 'if a default value was set' do + context 'and no custom value was given yet' do + it 'returns the default value' do + expect(record.setting_with_default).to eql default_value + end + end + + context 'and a custom value was given' do + it 'returns the custom value' do + record.setting_with_default = custom_value + expect(record.setting_with_default).to eql custom_value + end + end + end + + context 'if no default value was set' do + it 'returns nil' do + expect(record.setting_without_default).to be nil + end + end + end + + shared_examples 'setters' do + it 'creates a setter method' do + expect(record).to respond_to(:setting_with_default=) + end + + it 'marks the attribute as changed when a new value is assigned' do + record.setting_with_default = custom_value + expect(record.setting_with_default_changed?).to be true + end + + it 'marks the record as changed when a new value is assigned' do + record.setting_with_default = custom_value + expect(record).to be_changed + end + + it 'typecasts the value correctly' do + record.setting_with_default = raw_custom_value + expect(record.setting_with_default).to eql custom_value + expect(record.setting_with_default_before_type_cast).to eql raw_custom_value + end + + it 'provides access to the old value until the record is saved' do + record.setting_with_default = custom_value + expect(record.setting_with_default_was).to eql default_value + record.save! + record.setting_with_default = default_value + expect(record.setting_with_default_was).to eql custom_value + + # Sub-test for nil values + record.setting_without_default = custom_value + expect(record.setting_without_default_was).to be nil + end + end + + context 'when setting up a boolean setting' do + with_model 'TestModel' do + model do + setting_accessor :setting_with_default, type: :boolean, default: true + setting_accessor :setting_without_default, type: :boolean + end + end + + let(:default_value) { true } + let(:custom_value) { false } + let(:raw_custom_value) { 'false' } + + context 'regarding setters' do + include_examples 'setters' + end + + context 'regarding getters' do + include_examples 'getters' do + it 'creates a ?-alias for the getter' do + expect(record).to respond_to(:setting_with_default?) + expect(record.setting_with_default?).to eql record.setting_with_default + end + end + end + end + + context 'when setting up a string setting' do + with_model 'TestModel' do + model do + setting_accessor :setting_with_default, type: :string, default: 'Oiski Poiski!' + setting_accessor :setting_without_default, type: :string + end + end + + let(:default_value) { 'Oiski Poiski!' } + let(:custom_value) { 'true' } + let(:raw_custom_value) { true } + + context 'regarding getters' do + include_examples 'getters' + end + + context 'regarding setters' do + include_examples 'setters' + end + end + + context 'when setting up an integer setting' do + with_model 'TestModel' do + model do + setting_accessor :setting_with_default, type: :integer, default: 42 + setting_accessor :setting_without_default, type: :integer + end + end + + let(:default_value) { 42 } + let(:custom_value) { 21 } + let(:raw_custom_value) { '21' } + + context 'regarding getters' do + include_examples 'getters' + end + + context 'regarding setters' do + include_examples 'setters' + end + end + + context 'when setting up a polymorphic setting' do + with_model 'TestModel' do + model do + setting_accessor :setting_with_default, type: :polymorphic, default: {} + setting_accessor :setting_without_default, type: :polymorphic + end + end + + let(:default_value) { {} } + let(:custom_value) { {oiski: 'Poiski'} } + let(:raw_custom_value) { custom_value } + + context 'regarding getters' do + include_examples 'getters' + end + + context 'regarding setters' do + include_examples 'setters' do + it 'does not share the default value between all settings' do + # yes, this happened... + another_record = TestModel.create + record.setting_with_default[:a] = :b + expect(another_record.setting_with_default).to eql default_value + end + + it 'marks the attribute as changed when the existing value is mutated' do + record.setting_with_default[:a] = :b + expect(record.setting_with_default_changed?).to be true + end + + it 'marks the record as changed when the existing value is mutated' do + record.setting_with_default[:a] = :b + expect(record).to be_changed + end + end + end + end + end +end diff --git a/spec/lib/setting_accessors/setting_scaffold_spec.rb b/spec/lib/setting_accessors/setting_scaffold_spec.rb new file mode 100644 index 0000000..8d38f5f --- /dev/null +++ b/spec/lib/setting_accessors/setting_scaffold_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +describe SettingAccessors::SettingScaffold, type: :model do + include SettingModel + + with_model 'User' + + #---------------------------------------------------------------- + # .method_missing + #---------------------------------------------------------------- + + describe 'method_missing' do + context 'when a possible getter is being called' do + context 'with an additional argument (assignable)' do + let(:assignable) { User.create } + + it 'forwards the call to Setting.get' do + expect(Setting).to receive(:get).with('foo', assignable) + Setting.foo(assignable) + end + end + + context 'with no additional arguments' do + it 'forwards the call to Setting.get' do + expect(Setting).to receive(:get).with('foo', nil) + Setting.foo + end + end + + context 'with more than one argument' do + it 'calls the original method_missing, leading to a NoMethodError' do + expect { Setting.foo('bar', 'baz') }.to raise_error NoMethodError + end + end + end + + context 'when a setter is being called' do + context 'with the new value as its argument' do + it 'forwards the call to Setting.create_or_update' do + expect(Setting).to receive(:set).with('foo', 'bar') + Setting.foo = 'bar' + end + end + end + end + + #---------------------------------------------------------------- + # .get + #---------------------------------------------------------------- + + describe '.get' do + context 'when being called without an assignable' do + context 'and no setting with that name exists yet' do + it 'returns nil' do + expect(Setting.get('foo')).to be nil + end + end + + context 'and a setting with that name already exists' do + it "returns the setting's value" do + Setting.create(name: 'foo', value: 'bar') + expect(Setting.get('foo')).to eql 'bar' + end + end + end + + context 'when being called with an assignable' do + let(:assignable) { User.create } + subject { Setting.get('foo', assignable) } + + before(:each) do + Setting.create(name: 'foo', value: 'bar') + Setting.create(name: 'foo', value: 'baz', assignable: User.create) + end + + context 'and no assigned setting with that name exists yet' do + it { is_expected.to be nil } + end + + context 'and an assigned setting with that name already exists' do + before(:each) { Setting.create(name: 'foo', value: 'BAM', assignable: assignable) } + + it { is_expected.to eql 'BAM' } + end + end + end + + #---------------------------------------------------------------- + # .set + #---------------------------------------------------------------- + + describe '.set' do + context 'when being called without an assignable' do + context 'and no corresponding setting already exists' do + it 'creates a new setting with the given value' do + expect { Setting.set('foo', 'bar') }.to change { Setting.count }.from(0).to(1) + expect(Setting.last.value).to eql 'bar' + end + end + + context 'and a corresponding setting already exists' do + let!(:setting) { Setting.create!(name: 'foo', value: 'bar') } + + it 'updates the existing setting' do + expect { Setting.set('foo', 'baz') }.not_to(change { Setting.count }) + expect(setting.reload.value).to eql 'baz' + end + end + end + + context 'when being called with an assignable' do + let(:assignable) { User.create } + + context 'and no corresponding setting already exists' do + it 'creates a new setting with the given value and assignable' do + expect { Setting.set('foo', 'bar', assignable: assignable) }.to change { Setting.count }.from(0).to(1) + expect(Setting.last).to have_attributes(assignable: assignable, value: 'bar') + end + end + + context 'and a corresponding setting already exists' do + let!(:setting) { Setting.create!(name: 'foo', value: 'bar', assignable: assignable) } + + it 'updates the existing setting' do + expect { Setting.set('foo', 'baz', assignable: assignable) }.not_to(change { Setting.count }) + expect(setting.reload.value).to eql 'baz' + end + end + end + end + + #---------------------------------------------------------------- + # .[]= + #---------------------------------------------------------------- + + describe '.[]=' do + context 'when being called without an assignable' do + it 'sets the global setting accordingly' do + expect(Setting).to receive(:set).with('foo', 'bar', assignable: nil) + Setting['foo'] = 'bar' + end + end + + context 'when being called with an assignable' do + let(:assignable) { User.create } + + it 'sets the assigned setting accordingly' do + expect(Setting).to receive(:set).with('foo', 'bar', assignable: assignable) + Setting['foo', assignable] = 'bar' + end + end + + context 'when being used with ||=' do + it "sets the setting value if it doesn't exist yet" do + expect(Setting.foo).to be nil + Setting['foo'] ||= 'bar' + expect(Setting.foo).to eql 'bar' + Setting['foo'] ||= 'baz' + expect(Setting.foo).to eql 'bar' + + assignable = User.create + + expect(Setting.foo(assignable)).to be nil + Setting['foo', assignable] ||= 'bar' + expect(Setting.foo(assignable)).to eql 'bar' + Setting['foo', assignable] ||= 'baz' + expect(Setting.foo(assignable)).to eql 'bar' + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..107d610 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'active_record' + +# Hold the test sqlite database in memory without actually creating additional files +ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:' + +require 'with_model' +require 'setting_accessors' +require_relative 'support/setting_model' +require_relative 'support/helpers' +require_relative 'support/matchers/converters' + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end + + config.extend WithModel + config.include Helpers +end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 0000000..29eaefb --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Helpers + def setting_names(klass) + SettingAccessors::Internal.setting_accessor_names(klass) + end +end diff --git a/spec/support/matchers/converters.rb b/spec/support/matchers/converters.rb new file mode 100644 index 0000000..15f8428 --- /dev/null +++ b/spec/support/matchers/converters.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# +# Specialized matcher to test whether a Converter processes its inputs correctly. +# It can either test for a given expected value or compare the result +# to ActiveRecord's internal mechanisms. +# Please note that the latter may differ between database adapters, so use with care. +# +# @example +# expect(SettingAccessors::Converters::StringConverter).to convert(1).to('1') +# +# @example Test whether the converter produces the same output as ActiveRecord +# expect(SettingAccessors::Converters::StringConverter).to convert(1).similar_to(User.first).on(:first_name) +# +RSpec::Matchers.define :convert do |value| + match do |converter_class| + @errors = [] + converter = converter_class.new(value) + converted_value = converter.convert + + unless instance_variable_defined?('@expected_value') + @expected_value = begin + @record.send("#{@attribute}=", value) + @record.send(@attribute) + end + end + + if converted_value != @expected_value + message = "Expected #{converter_class}" + message += "\n to convert #{value.inspect} to #{@expected_value.inspect}" + message += ",\n but actual output was #{converted_value.inspect}" + @errors << message + end + + @errors.empty? + end + + chain :to do |expected_value| + @expected_value = expected_value + end + + chain :similar_to do |record| + @record = record + end + + chain :on do |attribute| + @attribute = attribute + end + + failure_message do + @errors.join("\n") + end +end diff --git a/spec/support/setting_model.rb b/spec/support/setting_model.rb new file mode 100644 index 0000000..df6eaf2 --- /dev/null +++ b/spec/support/setting_model.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module SettingModel + def self.included(base) + base.with_model 'Setting' do + table do |t| + t.belongs_to :assignable, polymorphic: true, index: false + t.string :name + t.text :value + t.timestamps null: false + end + + model do + belongs_to :assignable, polymorphic: true + serialize :value + include SettingAccessors::SettingScaffold + + #---------------------------------------------------------------- + # Validations + #---------------------------------------------------------------- + + validates :name, + uniqueness: {scope: [:assignable_type, :assignable_id]}, + presence: true + end + end + end +end diff --git a/test/dummy/README.rdoc b/test/dummy/README.rdoc deleted file mode 100644 index dd4e97e..0000000 --- a/test/dummy/README.rdoc +++ /dev/null @@ -1,28 +0,0 @@ -== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -rake doc:app. diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile deleted file mode 100644 index ba6b733..0000000 --- a/test/dummy/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails.application.load_tasks diff --git a/test/dummy/app/assets/images/.keep b/test/dummy/app/assets/images/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/app/assets/javascripts/application.js b/test/dummy/app/assets/javascripts/application.js deleted file mode 100644 index 5bc2e1c..0000000 --- a/test/dummy/app/assets/javascripts/application.js +++ /dev/null @@ -1,13 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require_tree . diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css deleted file mode 100644 index a443db3..0000000 --- a/test/dummy/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any styles - * defined in the other CSS/SCSS files in this directory. It is generally better to create a new - * file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb deleted file mode 100644 index d83690e..0000000 --- a/test/dummy/app/controllers/application_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ApplicationController < ActionController::Base - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception -end diff --git a/test/dummy/app/controllers/concerns/.keep b/test/dummy/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb deleted file mode 100644 index de6be79..0000000 --- a/test/dummy/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/test/dummy/app/mailers/.keep b/test/dummy/app/mailers/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/app/models/.keep b/test/dummy/app/models/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/app/models/concerns/.keep b/test/dummy/app/models/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/app/models/post.rb b/test/dummy/app/models/post.rb deleted file mode 100644 index 791dcb5..0000000 --- a/test/dummy/app/models/post.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Post < ActiveRecord::Base -end diff --git a/test/dummy/app/models/setting.rb b/test/dummy/app/models/setting.rb deleted file mode 100644 index 4576931..0000000 --- a/test/dummy/app/models/setting.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# This model handles the management of system wide or record specific settings -# -# @attr [String] name -# The setting's name -# -# @attr [Object] value -# The setting's value. May be anything that can be serialized through YAML -# -# You can access global settings just like a normal class method, -# please have a look at #method_missing for more information. -# -# If not absolutely necessary, please **do not** create settings yourself -# through Setting.new, instead use #create_or_update instead. -# -# There are also some usage examples in the corresponding test. -# - -class Setting < ActiveRecord::Base - belongs_to :assignable, :polymorphic => true - - serialize :value - - include SettingAccessors::SettingScaffold - - #---------------------------------------------------------------- - # Validations - #---------------------------------------------------------------- - - validates :name, - :uniqueness => {:scope => [:assignable_type, :assignable_id]}, - :presence => true - - validates_with SettingAccessors::Validator - - # - # Makes accessing settings a little easier. - # Examples: - # - # #Loading **the value** of a global setting named "my_setting" - # Setting.my_setting - # - # #Setting **the value** of a global setting named "my_setting" - # Setting.my_setting = [1,2,3,4,5] - # - # #Loading **the value** of an assigned setting named "cool_setting" - # #+some_cool_user+ is here an instance of ActiveRecord::Base - # Setting.cool_setting(some_cool_user) - # - def self.method_missing(method, *args) - super(method, *args) if args.size > 1 - method_name = method.to_s - if method_name.last == '=' - self.create_or_update(method_name[0..method_name.length-2], args.first, nil, true) - else - self.get(method_name, args.first) - end - end -end \ No newline at end of file diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb deleted file mode 100644 index 75ebfb2..0000000 --- a/test/dummy/app/models/user.rb +++ /dev/null @@ -1,19 +0,0 @@ -class User < ActiveRecord::Base - - setting_accessor :polymorphic_setting, :type => :polymorphic, :default => {}, :fallback => :default - - setting_accessor :locale, :type => :string, :validations => {:presence => true} - - setting_accessor :a_string, :fallback => :default - - setting_accessor :a_number, :fallback => :global - - setting_accessor :a_boolean, :fallback => false - - setting_accessor :class_wise_truthy_boolean, :type => :boolean, :default => true, :fallback => :default - - setting_accessor :class_wise_with_value_fallback, :type => :string, :fallback => 'Oiski Poiski!' - - setting_accessor :class_wise_with_default_fallback, :type => :string, :fallback => :default, :default => 'Kapitanski' - -end diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb deleted file mode 100644 index 593a778..0000000 --- a/test/dummy/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - - Dummy - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> - <%= csrf_meta_tags %> - - - -<%= yield %> - - - diff --git a/test/dummy/bin/bundle b/test/dummy/bin/bundle deleted file mode 100755 index 66e9889..0000000 --- a/test/dummy/bin/bundle +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -load Gem.bin_path('bundler', 'bundle') diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails deleted file mode 100755 index 728cd85..0000000 --- a/test/dummy/bin/rails +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) -require_relative '../config/boot' -require 'rails/commands' diff --git a/test/dummy/bin/rake b/test/dummy/bin/rake deleted file mode 100755 index 1724048..0000000 --- a/test/dummy/bin/rake +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -require_relative '../config/boot' -require 'rake' -Rake.application.run diff --git a/test/dummy/config.ru b/test/dummy/config.ru deleted file mode 100644 index 5bc2a61..0000000 --- a/test/dummy/config.ru +++ /dev/null @@ -1,4 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Rails.application diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb deleted file mode 100644 index d2e37ad..0000000 --- a/test/dummy/config/application.rb +++ /dev/null @@ -1,25 +0,0 @@ -require File.expand_path('../boot', __FILE__) - -require 'rails/all' - -Bundler.require(*Rails.groups) -require "setting_accessors" - -module Dummy - class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - - config.active_support.test_order = :sorted - end -end - diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb deleted file mode 100644 index 6266cfc..0000000 --- a/test/dummy/config/boot.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml deleted file mode 100644 index 1c1a37c..0000000 --- a/test/dummy/config/database.yml +++ /dev/null @@ -1,25 +0,0 @@ -# SQLite version 3.x -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# -default: &default - adapter: sqlite3 - pool: 5 - timeout: 5000 - -development: - <<: *default - database: db/development.sqlite3 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <<: *default - database: db/test.sqlite3 - -production: - <<: *default - database: db/production.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb deleted file mode 100644 index ee8d90d..0000000 --- a/test/dummy/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the Rails application. -require File.expand_path('../application', __FILE__) - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb deleted file mode 100644 index ddf0e90..0000000 --- a/test/dummy/config/environments/development.rb +++ /dev/null @@ -1,37 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.cache_classes = false - - # Do not eager load code on boot. - config.eager_load = false - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false - - # Print deprecation notices to the Rails logger. - config.active_support.deprecation = :log - - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true - - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true -end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb deleted file mode 100644 index 47d3553..0000000 --- a/test/dummy/config/environments/production.rb +++ /dev/null @@ -1,83 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.cache_classes = true - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. - # config.action_dispatch.rack_cache = true - - # Disable Rails's static asset server (Apache or nginx will already do this). - config.serve_static_assets = false - - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier - # config.assets.css_compressor = :sass - - # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - - # Generate digests for assets URLs. - config.assets.digest = true - - # Version of your assets, change this if you want to expire all your assets. - config.assets.version = '1.0' - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # Set to :debug to see everything in the log. - config.log_level = :info - - # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets. - # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. - # config.assets.precompile += %w( search.js ) - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false -end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb deleted file mode 100644 index a103e91..0000000 --- a/test/dummy/config/environments/test.rb +++ /dev/null @@ -1,34 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false - - # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false - - # Disable request forgery protection in test environment. - config.action_controller.allow_forgery_protection = false - - # Print deprecation notices to the stderr. - config.active_support.deprecation = :stderr - - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true -end diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/test/dummy/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 59385cd..0000000 --- a/test/dummy/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! diff --git a/test/dummy/config/initializers/cookies_serializer.rb b/test/dummy/config/initializers/cookies_serializer.rb deleted file mode 100644 index 7a06a89..0000000 --- a/test/dummy/config/initializers/cookies_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.config.action_dispatch.cookies_serializer = :json \ No newline at end of file diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index 4a994e1..0000000 --- a/test/dummy/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb deleted file mode 100644 index ac033bf..0000000 --- a/test/dummy/config/initializers/inflections.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format. Inflections -# are locale specific, and you may define rules for as many different -# locales as you wish. All of these examples are active by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end - -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' -# end diff --git a/test/dummy/config/initializers/mime_types.rb b/test/dummy/config/initializers/mime_types.rb deleted file mode 100644 index dc18996..0000000 --- a/test/dummy/config/initializers/mime_types.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf diff --git a/test/dummy/config/initializers/session_store.rb b/test/dummy/config/initializers/session_store.rb deleted file mode 100644 index e766b67..0000000 --- a/test/dummy/config/initializers/session_store.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.config.session_store :cookie_store, key: '_dummy_session' diff --git a/test/dummy/config/initializers/setting_accessors.rb b/test/dummy/config/initializers/setting_accessors.rb deleted file mode 100644 index 093b3b4..0000000 --- a/test/dummy/config/initializers/setting_accessors.rb +++ /dev/null @@ -1 +0,0 @@ -SettingAccessors.setting_class = Setting \ No newline at end of file diff --git a/test/dummy/config/initializers/wrap_parameters.rb b/test/dummy/config/initializers/wrap_parameters.rb deleted file mode 100644 index 33725e9..0000000 --- a/test/dummy/config/initializers/wrap_parameters.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# This file contains settings for ActionController::ParamsWrapper which -# is enabled by default. - -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) -end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml deleted file mode 100644 index 0653957..0000000 --- a/test/dummy/config/locales/en.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t 'hello' -# -# In views, this is aliased to just `t`: -# -# <%= t('hello') %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. - -en: - hello: "Hello world" diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb deleted file mode 100644 index 3f66539..0000000 --- a/test/dummy/config/routes.rb +++ /dev/null @@ -1,56 +0,0 @@ -Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end -end diff --git a/test/dummy/config/secrets.yml b/test/dummy/config/secrets.yml deleted file mode 100644 index fd59d1a..0000000 --- a/test/dummy/config/secrets.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: 9466152fe1a4826b5d3df6a72834d41c3771e28d1d9aa4bbdc46f5882bf118f2c1baba5ce51f0adeb66d1ee452c10635c5dcedcf6a94b92dfef756d3b75f95f5 - -test: - secret_key_base: 0ed95a983cd27bdf250be838d482830715643bf7f110971bdac935a2d3437c1c2f5a8f6bc7d3fc97f8819a4f1628950311952e219783d53d810f043bf8708972 - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/test/dummy/config/settings.yml b/test/dummy/config/settings.yml deleted file mode 100644 index 3524374..0000000 --- a/test/dummy/config/settings.yml +++ /dev/null @@ -1,23 +0,0 @@ -# -# This file specifies all settings used in the application -# (be it record specific or global). -# -# The keys are the setting names, the values are a hash -# containing validation options, type and default value -# - -a_string: - type: string - default: "I am a string!" - validations: - presence: true - -a_number: - type: integer - default: "I am a Number!" - -a_boolean: - type: boolean - default: true - validations: - boolean: true \ No newline at end of file diff --git a/test/dummy/db/migrate/20150102112106_create_users.rb b/test/dummy/db/migrate/20150102112106_create_users.rb deleted file mode 100644 index 2f69d27..0000000 --- a/test/dummy/db/migrate/20150102112106_create_users.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateUsers < ActiveRecord::Migration - def change - create_table :users do |t| - t.string :first_name - t.string :last_name - t.timestamps null: false - end - end -end diff --git a/test/dummy/db/migrate/20150102115329_create_settings.rb b/test/dummy/db/migrate/20150102115329_create_settings.rb deleted file mode 100644 index ce66188..0000000 --- a/test/dummy/db/migrate/20150102115329_create_settings.rb +++ /dev/null @@ -1,12 +0,0 @@ -class CreateSettings < ActiveRecord::Migration - def change - create_table :settings do |t| - t.belongs_to :assignable, :polymorphic => true - - t.string :name - t.text :value - - t.timestamps - end - end -end \ No newline at end of file diff --git a/test/dummy/db/migrate/20150723114600_create_posts.rb b/test/dummy/db/migrate/20150723114600_create_posts.rb deleted file mode 100644 index 4bf0a8f..0000000 --- a/test/dummy/db/migrate/20150723114600_create_posts.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreatePosts < ActiveRecord::Migration - def change - create_table :posts do |t| - t.belongs_to :user - t.string :title - t.string :text - - t.timestamps null: false - end - end -end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb deleted file mode 100644 index 4fa8eaa..0000000 --- a/test/dummy/db/schema.rb +++ /dev/null @@ -1,40 +0,0 @@ -# encoding: UTF-8 -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20150723114600) do - - create_table "posts", force: :cascade do |t| - t.integer "user_id" - t.string "title" - t.string "text" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "settings", force: :cascade do |t| - t.integer "assignable_id" - t.string "assignable_type" - t.string "name" - t.text "value" - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "users", force: :cascade do |t| - t.string "first_name" - t.string "last_name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - -end diff --git a/test/dummy/db/test.sqlite3 b/test/dummy/db/test.sqlite3 deleted file mode 100644 index 6a5c7cb..0000000 Binary files a/test/dummy/db/test.sqlite3 and /dev/null differ diff --git a/test/dummy/lib/assets/.keep b/test/dummy/lib/assets/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/log/.keep b/test/dummy/log/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/public/404.html b/test/dummy/public/404.html deleted file mode 100644 index b612547..0000000 --- a/test/dummy/public/404.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/test/dummy/public/422.html b/test/dummy/public/422.html deleted file mode 100644 index a21f82b..0000000 --- a/test/dummy/public/422.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/test/dummy/public/500.html b/test/dummy/public/500.html deleted file mode 100644 index 061abc5..0000000 --- a/test/dummy/public/500.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - - -
-
-

We're sorry, but something went wrong.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/test/dummy/public/favicon.ico b/test/dummy/public/favicon.ico deleted file mode 100644 index e69de29..0000000 diff --git a/test/dummy/test/fixtures/posts.yml b/test/dummy/test/fixtures/posts.yml deleted file mode 100644 index 937a0c0..0000000 --- a/test/dummy/test/fixtures/posts.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/dummy/test/models/post_test.rb b/test/dummy/test/models/post_test.rb deleted file mode 100644 index f3a4e2a..0000000 --- a/test/dummy/test/models/post_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_relative '../../../test_helper' - -class PostTest < ActiveSupport::TestCase - context 'JSON serialization' do - setup do - @post = Post.new(:title => 'a post', :text => 'a content') - end - - should 'work without having any setting_accessors defined' do - assert @post.as_json - end - - should 'contain all original public attributes' do - [:title, :text, :id, :created_at, :updated_at].each do |attr| - assert_includes @post.as_json.keys, attr.to_s - end - end - end -end diff --git a/test/dummy/test/models/setting_test.rb b/test/dummy/test/models/setting_test.rb deleted file mode 100644 index 35de427..0000000 --- a/test/dummy/test/models/setting_test.rb +++ /dev/null @@ -1,143 +0,0 @@ -require_relative '../../../test_helper' - -class SettingTest < ActiveSupport::TestCase - should validate_presence_of :name - - #FIXME: This is currently setting assignable_type and assignable_id to "0" resp. "1" which breaks everything. - # should validate_uniqueness_of(:name).scoped_to([:assignable_type, :assignable_id]) - - context 'Global Setting Accessors (method missing)' do - - should 'ignore a setting function if more than 1 argument is given' do - assert_raises(NoMethodError) { Setting.my_setting(1, 2, 3) } - end - - should 'return nil for a non-existing setting' do - assert_nil Setting.gotta_catch_em_all - end - - should 'create a new setting if necessary' do - assert Setting.count.zero? - assert Setting.gotta_catch_em_all = 'Pokemon!' - assert Setting.count == 1 - end - - should "return the setting's value for an existing setting" do - assert Setting.gotta_catch_em_all = 'Pokemon!' - assert_equal Setting.gotta_catch_em_all, 'Pokemon!' - end - - should 'update an existing setting instead of creating a new one' do - assert Setting.count.zero? - assert Setting.gotta_catch_em_all = 'Pokemon!' - assert Setting.gotta_catch_em_all = 'Pokemon!' - assert Setting.count == 1 - end - - should 'return assignable specific settings if an assignable is given' do - ash = User.create(:first_name => 'Ash', :last_name => 'Ketchum') - gary = User.create(:first_name => 'Gary', :last_name => 'Oak') - team_rocket = User.create(:first_name => 'Jessie', :last_name => 'James') - - assert Setting.create_or_update(:pokedex_count, 151, ash) - assert Setting.create_or_update(:pokedex_count, 1, gary) - - assert_nil Setting.pokedex_count - - assert_equal Setting.pokedex_count(ash), 151 - assert_equal Setting.pokedex_count(gary), 1 - - #They don't want to be on file. - assert_nil Setting.pokedex_count(team_rocket) - end - end - - context 'The create_or_update function' do - setup do - @ash = User.create(:first_name => 'Ash', :last_name => 'Ketchum') - end - - should 'create a new assigned setting if it did not exist before' do - assert Setting.count.zero? - Setting.create_or_update(:pokedex_count, 151, @ash) - assert Setting.count == 1 - end - - should 'update an assigned setting if it already exists' do - Setting.create_or_update(:pokedex_count, 150, @ash) - assert Setting.count == 1 - Setting.create_or_update(:pokedex_count, 151, @ash) - assert Setting.count == 1 - assert_equal Setting.pokedex_count(@ash), 151 - end - - should 'return a setting object by default' do - assert_instance_of Setting, Setting.create_or_update(:pokedex_count, 151, @ash) - end - - should 'return just the value if wished' do - assert_equal Setting.create_or_update(:pokedex_count, 151, @ash, true), 151 - end - end - - context 'The globally_defined method' do - should 'return true for a setting which is defined in config/settings.yml' do - assert SettingAccessors::Internal.globally_defined_setting?('a_string') - end - - should 'return false for a setting which is not defined in config/settings.yml' do - assert !SettingAccessors::Internal.globally_defined_setting?('something_different') - end - end - - context 'setting_accessors' do - should 'return the default value as fallback if :fallback => :default is given' do - assert_equal 'I am a string!', User.new.a_string - end - - should 'return the global setting value as fallback if :fallback => :global is given' do - Setting.a_number = 42 - assert_equal 42, User.new.a_number - end - - should 'return the given value as fallback if :fallback => VALUE is given' do - assert_equal false, User.new.a_boolean - end - end - - context 'Class-Defined Settings' do - should 'have the type defined in the setting_accessor call' do - assert u = User.create(:first_name => 'a', :last_name => 'name', :locale => 'de') - assert s = Setting.setting_record(:locale, User.first) - assert_equal 'string', s.value_type.to_s - end - - should 'not override global settings' do - assert_raises(ArgumentError) do - User.class_eval do - setting_accessor :a_string, :type => :integer - end - end - end - - should 'use value fallbacks' do - assert_equal 'Oiski Poiski!', User.new.class_wise_with_value_fallback - end - - should 'use default fallbacks' do - assert_equal 'Kapitanski', User.new.class_wise_with_default_fallback - end - end - - context 'Boolean settings' do - should 'respect temporary settings which are set to `false`' do - user = User.new(:class_wise_truthy_boolean => false) - assert_equal user.class_wise_truthy_boolean, false - end - - should 'respect temporary settings which are set to `true`' do - user = User.new(:a_boolean => true) - assert_equal user.a_boolean, true - end - end -end diff --git a/test/dummy/test/models/user_test.rb b/test/dummy/test/models/user_test.rb deleted file mode 100644 index 0facda9..0000000 --- a/test/dummy/test/models/user_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require_relative '../../../test_helper' - -class UserTest < ActiveSupport::TestCase - context 'JSON serialization' do - setup do - @user = User.new(:a_string => 'test', :a_number => 42, :a_boolean => false) - end - - should 'include the setting accessors' do - SettingAccessors::Internal.setting_accessor_names(User).each do |setting_name| - assert_includes @user.as_json.keys, setting_name - end - end - - should 'contain the correct values' do - SettingAccessors::Internal.setting_accessor_names(User).each do |setting_name| - assert_equal @user.as_json[setting_name.to_s], @user.send(setting_name) - end - end - - context 'when using as_json with option :only' do - context 'set to a class attribute name' do - should 'not include any setting accessors' do - json = @user.as_json :only => [:first_name] - SettingAccessors::Internal.setting_accessor_names(User).each do |setting_name| - assert_not_includes json.keys, setting_name - end - end - end - - context 'set to a setting accessor name' do - should 'not include other setting accessors' do - json = @user.as_json :only => [:a_string] - assert_includes json.keys, 'a_string' - (SettingAccessors::Internal.setting_accessor_names(User) - ['a_string']).each do |setting_name| - assert_not_includes json.keys, setting_name - end - end - end - end - - context 'when using as_json with option :except' do - context 'set to a class attribute name' do - should 'include all setting accessors' do - json = @user.as_json :except => [:first_name] - SettingAccessors::Internal.setting_accessor_names(User).each do |setting_name| - assert_includes json.keys, setting_name - end - end - end - - context 'set to a setting accessor name' do - should 'include all other setting accessors' do - json = @user.as_json :except => [:a_string] - assert_not_includes json.keys, 'a_string' - (SettingAccessors::Internal.setting_accessor_names(User) - ['a_string']).each do |setting_name| - assert_includes json.keys, setting_name - end - end - end - end - end - - context 'Boolean getter methods' do - setup do - @user = User.new(:a_string => 'test', :a_number => 42, :a_boolean => false) - end - - should 'be created for boolean settings' do - assert @user.respond_to?(:a_boolean?), '?-getter is not defined for boolean settings' - end - - should 'return the same value as the original getter' do - assert_equal @user.a_boolean, @user.a_boolean? - end - - should 'not be created for non-boolean settings' do - assert !@user.respond_to?(:a_number?) - assert !@user.respond_to?(:a_string?) - end - end - - context 'the read set (@temp_settings)' do - setup do - @user = User.create - @user_alias = User.find(@user.id) - - # Use @user_alias here to ensure that the setting value is saved in the instance's read set - @user.a_boolean = !@user_alias.a_boolean - assert @user.save - end - - should 'be refreshed with the new values on #reload' do - assert @user_alias.reload - assert_equal @user.a_boolean, @user_alias.a_boolean - end - end - - context 'Polymorphic class-wise settings' do - setup do - @user = User.create - end - - context 'when being assigned an initial value' do - should 'be created in database' do - @user.polymorphic_setting = {:a => :b} - assert @user.save - assert_equal User.last, @user - assert_equal User.last.polymorphic_setting, {:a => :b} - end - - should 'be created in database if one of their properties changes' do - @user.polymorphic_setting[:new_key] = 'new_value' - assert @user.save - assert_equal User.last, @user - assert_equal({:new_key => 'new_value'}, User.last.polymorphic_setting) - end - - should 'not change the value of other assignable settings' do - @user2 = User.create - @user2.polymorphic_setting = {:foo => :bar} - assert @user2.save - assert_equal User.first.polymorphic_setting, {} - end - end - - context 'when being updated' do - setup do - @user.polymorphic_setting = {:a => :b} - assert @user.save - assert @user.reload - assert_equal({:a => :b}, @user.polymorphic_setting) - assert_equal({:a => :b}, User.last.polymorphic_setting) - end - - # Single hash value changed, etc. - should 'be saved if one of their properties changes' do - @user.polymorphic_setting[:a] = :c - assert @user.save - assert @user.reload - assert_equal({:a => :c}, @user.polymorphic_setting) - assert_equal({:a => :c}, User.last.polymorphic_setting) - end - - should 'be updated if their whole value changes' do - @user.polymorphic_setting = {:a => :c} - assert @user.save - assert @user.reload - assert_equal({:a => :c}, @user.polymorphic_setting) - assert_equal({:a => :c}, User.last.polymorphic_setting) - end - end - end -end diff --git a/test/generators/install_generator_test.rb b/test/generators/install_generator_test.rb deleted file mode 100644 index ce0c281..0000000 --- a/test/generators/install_generator_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require_relative '../test_helper' - -class InstallGeneratorTest < Rails::Generators::TestCase - tests SettingAccessors::Generators::InstallGenerator - destination File.expand_path('../dummy/tmp', File.dirname(__FILE__)) - setup :prepare_destination - - test 'Assert all files are properly created' do - run_generator - assert_file 'config/settings.yml' - assert_file 'config/initializers/setting_accessors.rb' - assert_file 'app/models/setting.rb' - assert_migration 'db/migrate/create_settings.rb' - end -end \ No newline at end of file diff --git a/test/setting_accessors_test.rb b/test/setting_accessors_test.rb deleted file mode 100644 index cde7eb0..0000000 --- a/test/setting_accessors_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require_relative 'test_helper' - -class SettingAccessorsTest < ActiveSupport::TestCase -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 25e55bd..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,31 +0,0 @@ -# Configure Rails Environment -ENV["RAILS_ENV"] = "test" - -require File.expand_path("../dummy/config/environment.rb", __FILE__) - -require 'active_support' -require 'minitest' -require 'shoulda' -require "rails/test_help" - -Rails.backtrace_cleaner.remove_silencers! - -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - -# Load fixtures from the engine -if ActiveSupport::TestCase.method_defined?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) -end - -# configure shoulda matchers -Shoulda::Matchers.configure do |config| - config.integrate do |with| - with.test_framework :minitest - with.library :rails - end -end - -# For generators -require "rails/generators/test_case" -require "generators/setting_accessors/install_generator"