├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── active_params.gemspec ├── bin ├── console └── setup ├── lib ├── active_params.rb └── active_params │ ├── controller.rb │ ├── parser.rb │ └── version.rb └── test ├── active_params └── parser_test.rb ├── active_params_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.12.5 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_params.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chew Choon Keat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveParams 2 | 3 | Stop manually defining `strong_parameters` in each and every controller. 4 | 5 | Whatever parameters that was used during `development` mode is considered permitted parameters for `production`. So automatically record them in `development` mode and simply apply `strong_parameters` in production. 6 | 7 | aka No more [strong parameters falls!](https://twitter.com/JuanitoFatas/status/746228574592499712) 👌 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'active_params' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install active_params 24 | 25 | ## Usage 26 | 27 | Include the `ActiveParams` module into your controller, e.g. 28 | 29 | ```ruby 30 | class ApplicationController < ActionController::Base 31 | include ActiveParams 32 | end 33 | ``` 34 | 35 | ### Rails.env.development? 36 | 37 | During `development` mode, `active_params` will generate the appropriate strong parameters settings for each param (scoped to the current http method, controller_name and action_name) 38 | 39 | For example, when you submit a form like this 40 | 41 | ``` 42 | Started POST "/users" for 127.0.0.1 at 2016-06-26 00:41:19 +0800 43 | Processing by UsersController#create as HTML 44 | Parameters: {"utf8"=>"✓", "authenticity_token"=>"...", "user"=>{"name"=>"John", "avatar"=>"Face.png", "photos"=>["Breakfast.png", "Coffee.png"]}, "button"=>""} 45 | ``` 46 | 47 | `active_params` will create or update the `config/active_params.json` file with the settings 48 | 49 | ``` json 50 | { 51 | "POST users/create": { 52 | "user": [ 53 | "avatar", 54 | "name", 55 | { 56 | "photos": [ 57 | 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | ``` 64 | 65 | NOTE: see [the test](https://github.com/choonkeat/active_params/blob/a84e0ab41ee7a522c6c38ee1657cfb68bc4850e9/test/active_params_test.rb#L23-L57) for a more complicated example 66 | 67 | ### Rails.env.production? (and other modes) 68 | 69 | In non-development mode, `active_params` will NOT update the `config/active_params.json` file. 70 | 71 | It loads `config/active_params.json` and automatically perform the correct strong parameters setup for each request 72 | 73 | ``` ruby 74 | params[:user] = params.require(:user).permit(:avatar, :name, photos: []) 75 | ``` 76 | 77 | So, in your controllers, you can simply reference `params[:user]` again! 78 | 79 | ``` ruby 80 | def create 81 | @user = User.new(params[:user]) 82 | if @user.save 83 | redirect_to @user, notice: 'User was successfully created.' 84 | else 85 | render :new 86 | end 87 | end 88 | ``` 89 | 90 | ### Workflow 91 | 92 | When you create new features for your app & try them out in development mode, `config/active_params.json` will be automatically updated. When you commit your code, include the changes to `config/active_params.json` too. 93 | 94 | ### Static Customizations 95 | 96 | You can add a `config/initializers/active_params.rb` file in your Rails app, and perform a global config like this 97 | 98 | ```ruby 99 | ActiveParams.config do |config| 100 | config.writing = Rails.env.development? 101 | config.path = "config/active_params.json" 102 | config.scope = proc { |controller| 103 | "#{controller.request.method} #{controller.controller_name}/#{controller.action_name}" 104 | } 105 | end 106 | ``` 107 | 108 | ### Dynamic Customizations 109 | 110 | Each controller may need its own config, e.g. some strong params are only permitted for certain current_user roles. For such controllers, use the `include ActiveParams.setup(...)` syntax instead 111 | 112 | ```ruby 113 | class ApplicationController < ActionController::Base 114 | include ActiveParams.setup({ 115 | path: "tmp/strong_params.json", 116 | writing: !Rails.env.production?, 117 | scope: proc {|ctrl| 118 | [ctrl.current_user.role, ActiveParams.scope.(ctrl)].join('@') 119 | }, 120 | }) 121 | end 122 | ``` 123 | 124 | You can setup 125 | - where the config file is stored 126 | - if you want to write to the config file 127 | - how should strong params be scoped 128 | 129 | ## LICENSE 130 | 131 | MIT 132 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /active_params.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_params/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_params" 8 | spec.version = ActiveParams::VERSION 9 | spec.authors = ["choonkeat"] 10 | spec.email = ["choonkeat@gmail.com"] 11 | 12 | spec.summary = %q{Automatic strong parameters} 13 | spec.description = %q{Bye bye strong parameters falls} 14 | spec.homepage = "http://github.com/choonkeat/active_params" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.12" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | spec.add_development_dependency "minitest" 24 | end 25 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "active_params" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/active_params.rb: -------------------------------------------------------------------------------- 1 | require "active_params/version" 2 | require "active_params/parser" 3 | require "active_params/controller" 4 | 5 | module ActiveParams 6 | class << self 7 | attr_accessor :writing, :path, :scope 8 | 9 | # ActiveParams.config {|c| .... } 10 | def config 11 | yield self 12 | end 13 | 14 | # `include ActiveParams` use default settings 15 | def included(base) 16 | base.send(:include, setup) 17 | end 18 | 19 | # `include ActiveParams.setup({...})` customize 20 | def setup(options = {}) 21 | Module.new do 22 | def self.included(base) 23 | base.send(:include, ActiveParams::Controller) 24 | end 25 | end.tap do |m| 26 | m.send(:define_method, :active_params_options) { options } 27 | end 28 | end 29 | end 30 | end 31 | 32 | ActiveParams.config do |config| 33 | config.writing = defined?(Rails) && Rails.env.development? 34 | config.path = "config/active_params.json" 35 | config.scope = proc { |controller| 36 | "#{controller.request.method} #{controller.controller_name}/#{controller.action_name}" 37 | } 38 | end 39 | -------------------------------------------------------------------------------- /lib/active_params/controller.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module ActiveParams 4 | module Controller 5 | def self.included(base) 6 | add_before_method = 7 | (base.methods.include?(:before_action) && :before_action) || 8 | (base.methods.include?(:before_filter) && :before_filter) 9 | 10 | if add_before_method 11 | base.class_eval do 12 | send add_before_method, :active_params_write, if: :active_params_writing? 13 | send add_before_method, :active_params_apply 14 | end 15 | end 16 | end 17 | 18 | def active_params_writing? 19 | active_params_options[:writing] || ActiveParams.writing 20 | end 21 | 22 | def active_params_path 23 | active_params_options[:path] || ActiveParams.path 24 | end 25 | 26 | def active_params_json 27 | @@active_params_json ||= (File.exists?(active_params_path) ? JSON.parse(IO.read active_params_path) : {}) 28 | rescue JSON::ParserError 29 | return {} 30 | ensure 31 | # undo cache in development mode 32 | @@active_params_json = nil if active_params_writing? 33 | end 34 | 35 | def active_params_scope 36 | scope = active_params_options[:scope] || ActiveParams.scope 37 | scope.respond_to?(:call) ? scope.(self) : scope 38 | end 39 | 40 | def active_params_write(global_json: active_params_json) 41 | scoped_json = global_json[active_params_scope] ||= {} 42 | params.each do |k,v| 43 | case result = ActiveParams::Parser.strong_params_definition_for(v) 44 | when Array, Hash 45 | scoped_json[k] = result 46 | end 47 | end 48 | open(active_params_path, "wb") {|f| f.write(JSON.pretty_generate(global_json)) } 49 | end 50 | 51 | def active_params_apply(global_json: active_params_json) 52 | # e.g. POST users#update 53 | scoped_json = global_json[active_params_scope] ||= {} 54 | params.each do |k,v| 55 | if result = scoped_json[k] 56 | params[k] = params.require(k).permit(result) 57 | end 58 | end 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /lib/active_params/parser.rb: -------------------------------------------------------------------------------- 1 | module ActiveParams 2 | module Parser 3 | # to obtain a hash of all possible keys 4 | def combine_hashes(array_of_hashes) 5 | array_of_hashes.select {|v| v.kind_of?(Hash) }. 6 | inject({}) {|sum, hash| hash.inject(sum) {|sum,(k,v)| sum.merge(k => v) } } 7 | end 8 | 9 | def strong_params_definition_for(params) 10 | if params.kind_of?(Array) 11 | combined_hash = combine_hashes(params) 12 | if combined_hash.empty? 13 | [] 14 | else 15 | strong_params_definition_for(combined_hash) 16 | end 17 | elsif params.respond_to?(:keys) # Hash, ActionController::Parameters 18 | values, arrays = [[], {}] 19 | params.each do |key, value| 20 | case value 21 | when Array 22 | arrays[key] = strong_params_definition_for(value) 23 | when Hash 24 | arrays[key] = strong_params_definition_for(combine_hashes(value.values)) 25 | else 26 | values.push(key) 27 | end 28 | end 29 | if arrays.empty? 30 | values 31 | else 32 | [*values.sort, arrays] 33 | end 34 | else 35 | params 36 | end 37 | end 38 | 39 | self.extend(self) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/active_params/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveParams 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/active_params/parser_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ActiveParams::ParserTest < Minitest::Test 4 | include ActiveParams::Parser 5 | 6 | def test_strong_params_definition_for_number 7 | assert_equal 123, 8 | strong_params_definition_for(123) 9 | end 10 | 11 | def test_strong_params_definition_for_string 12 | assert_equal "123", 13 | strong_params_definition_for("123") 14 | end 15 | 16 | def test_strong_params_definition_for_array 17 | assert_equal [], 18 | strong_params_definition_for([123, "123"]) 19 | end 20 | 21 | def test_strong_params_definition_for 22 | assert_equal [:id, :name, contacts: [:name, :relation, contacts: [:relation, :name], addresses: [:label, :address]], interests: []], 23 | strong_params_definition_for({ 24 | id: rand, 25 | name: "Lorem ipsum", 26 | contacts: { 27 | "0" => { 28 | relation: "Friend", 29 | name: "Bob", 30 | }, 31 | "1" => { 32 | relation: "Mother", 33 | name: "Charlie", 34 | contacts: { 35 | "0" => { 36 | relation: "Friend", 37 | name: "David", 38 | }, 39 | "1" => { 40 | relation: "Friend", 41 | name: "Ethan", 42 | }, 43 | }, 44 | addresses: [ 45 | { label: "home", address: "123 Sesame Street, NY" }, 46 | { label: "work", address: "Lorem ipsum"}, 47 | ] 48 | }, 49 | }, 50 | interests: [ 51 | "Running", 52 | "Netflix", 53 | ] 54 | }) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/active_params_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ActiveParamsTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::ActiveParams::VERSION 6 | end 7 | 8 | # `include ActiveParams` 9 | # `ActiveParams.config` 10 | def test_default 11 | value = rand 12 | klass = Class.new do 13 | def request; Struct.new(:method).new("GET"); end 14 | def controller_name; "home"; end 15 | def action_name; "index"; end 16 | include ActiveParams 17 | end 18 | instance = klass.new 19 | assert_equal nil, instance.active_params_writing? 20 | assert_equal ENV.fetch("ACTIVE_PARAMS_PATH", "config/active_params.json"), instance.active_params_path 21 | assert_equal "GET home/index", instance.active_params_scope 22 | end 23 | 24 | # `include ActiveParams.setup...` 25 | def test_setup_writing 26 | value = rand 27 | klass = Class.new do 28 | include ActiveParams.setup(writing: value) 29 | end 30 | assert_equal value, klass.new.active_params_writing? 31 | end 32 | 33 | def test_setup_path 34 | value = rand 35 | klass = Class.new do 36 | include ActiveParams.setup(path: value) 37 | end 38 | assert_equal value, klass.new.active_params_path 39 | end 40 | 41 | def test_setup_scope 42 | value = rand 43 | klass = Class.new do 44 | include ActiveParams.setup(scope: proc { value }) 45 | end 46 | assert_equal value, klass.new.active_params_scope 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'active_params' 3 | 4 | require 'minitest/autorun' 5 | --------------------------------------------------------------------------------