rubanok 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e340829681d2cdd077e745b2bf858ae9174eb0782c93a5665b86970b40652a67
4
- data.tar.gz: 7e07185d8f569c23e67f48a9905725b3cd1ba2401933bb68c9bdc1f25ba927e0
3
+ metadata.gz: c22d92e478499cec879d532e120f57d6a07f2655c3f0ba112b613e5e4123c97e
4
+ data.tar.gz: 2010c2271258c7a39cace169a0a6eaf7b90c1c2029392a1d5ec4e9dce49c91be
5
5
  SHA512:
6
- metadata.gz: 2fb0ef7961c6f9954d59e31f94ab9dfc034a15fe8edb4a611ffd016ee4a81159fb2272a249e6d8693ce45156e38890dc84069def6113cfc90660b5d2e481bbbb
7
- data.tar.gz: d07c80bdb6904f1767f69d2f933b5138edef0c11611f29bf0b75dfef1b8aae8383ccc3b2a8e42edccac45842773553a2efe535b3ae95eb60a3163f35f5cf7d3f
6
+ metadata.gz: 1ae97af6ca1cb1e7ab77f0ac84011a6dcb31882415d96ed6cf89f55d74c47898187d653285b130d7e80b8e97e23d8c23e106954efb455f89189346fad05e3b84
7
+ data.tar.gz: 35296df1a1434889966c90bd3c309ac25c5a6719035b8088b4964d5071fc8ad9e509833b4a8851bd576bde727a344c5c43610e423247d639953192bc9f1b068d
@@ -0,0 +1,63 @@
1
+ require:
2
+ - standard/cop/semantic_blocks
3
+ - rubocop-rspec
4
+
5
+ inherit_gem:
6
+ standard: config/base.yml
7
+
8
+ AllCops:
9
+ Exclude:
10
+ - 'bin/*'
11
+ - 'tmp/**/*'
12
+ - 'Gemfile'
13
+ - 'vendor/**/*'
14
+ - 'gemfiles/**/*'
15
+ DisplayCopNames: true
16
+
17
+ Standard/SemanticBlocks:
18
+ Enabled: false
19
+
20
+ Style/TrailingCommaInArrayLiteral:
21
+ EnforcedStyleForMultiline: no_comma
22
+
23
+ Style/TrailingCommaInHashLiteral:
24
+ EnforcedStyleForMultiline: no_comma
25
+
26
+ Style/FrozenStringLiteralComment:
27
+ Enabled: true
28
+
29
+ RSpec/Focus:
30
+ Enabled: true
31
+
32
+ RSpec/EmptyExampleGroup:
33
+ Enabled: true
34
+
35
+ RSpec/EmptyLineAfterExampleGroup:
36
+ Enabled: true
37
+
38
+ RSpec/EmptyLineAfterFinalLet:
39
+ Enabled: true
40
+
41
+ RSpec/EmptyLineAfterHook:
42
+ Enabled: true
43
+
44
+ RSpec/EmptyLineAfterSubject:
45
+ Enabled: true
46
+
47
+ RSpec/HooksBeforeExamples:
48
+ Enabled: true
49
+
50
+ RSpec/ImplicitExpect:
51
+ Enabled: true
52
+
53
+ RSpec/IteratedExpectation:
54
+ Enabled: true
55
+
56
+ RSpec/LetBeforeExamples:
57
+ Enabled: true
58
+
59
+ RSpec/MissingExampleGroupArgument:
60
+ Enabled: true
61
+
62
+ RSpec/ReceiveCounts:
63
+ Enabled: true
@@ -1,5 +1,20 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.5.1
5
- before_install: gem install bundler -v 1.16.2
4
+ - 2.6.0
5
+
6
+ notifications:
7
+ email: false
8
+
9
+ matrix:
10
+ fast_finish: true
11
+ include:
12
+ - rvm: ruby-head
13
+ gemfile: gemfiles/railsmaster.gemfile
14
+ - rvm: 2.6.0
15
+ gemfile: gemfiles/rails52.gemfile
16
+ - rvm: 2.5.1
17
+ gemfile: gemfiles/rails42.gemfile
18
+ allow_failures:
19
+ - rvm: ruby-head
20
+ gemfile: gemfiles/railsmaster.gemfile
@@ -0,0 +1,11 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.1.0 (2019-01-04)
6
+
7
+ Initial implementation.
8
+
9
+ ## 0.0.1 (2018-12-07)
10
+
11
+ Proposal added.
data/Gemfile CHANGED
@@ -1,6 +1,13 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
3
  # Specify your gem's dependencies in rubanok.gemspec
6
4
  gemspec
5
+
6
+ gem "pry-byebug", platform: :mri
7
+ gem "simplecov"
8
+
9
+ local_gemfile = 'Gemfile.local'
10
+
11
+ if File.exist?(local_gemfile)
12
+ eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
13
+ end
@@ -0,0 +1,2 @@
1
+ gem "actionpack", "~> 5.2"
2
+ gem "actionview", "~> 5.2"
@@ -0,0 +1,136 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rubanok (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ actionpack (5.2.2)
10
+ actionview (= 5.2.2)
11
+ activesupport (= 5.2.2)
12
+ rack (~> 2.0)
13
+ rack-test (>= 0.6.3)
14
+ rails-dom-testing (~> 2.0)
15
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
16
+ actionview (5.2.2)
17
+ activesupport (= 5.2.2)
18
+ builder (~> 3.1)
19
+ erubi (~> 1.4)
20
+ rails-dom-testing (~> 2.0)
21
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
22
+ activesupport (5.2.2)
23
+ concurrent-ruby (~> 1.0, >= 1.0.2)
24
+ i18n (>= 0.7, < 2)
25
+ minitest (~> 5.1)
26
+ tzinfo (~> 1.1)
27
+ ast (2.4.0)
28
+ builder (3.2.3)
29
+ byebug (10.0.2)
30
+ coderay (1.1.2)
31
+ concurrent-ruby (1.1.4)
32
+ crass (1.0.4)
33
+ diff-lcs (1.3)
34
+ docile (1.3.1)
35
+ erubi (1.8.0)
36
+ i18n (1.4.0)
37
+ concurrent-ruby (~> 1.0)
38
+ jaro_winkler (1.5.2)
39
+ json (2.1.0)
40
+ loofah (2.2.3)
41
+ crass (~> 1.0.2)
42
+ nokogiri (>= 1.5.9)
43
+ method_source (0.9.2)
44
+ mini_portile2 (2.4.0)
45
+ minitest (5.11.3)
46
+ nokogiri (1.10.0)
47
+ mini_portile2 (~> 2.4.0)
48
+ parallel (1.12.1)
49
+ parser (2.5.3.0)
50
+ ast (~> 2.4.0)
51
+ powerpack (0.1.2)
52
+ pry (0.12.2)
53
+ coderay (~> 1.1.0)
54
+ method_source (~> 0.9.0)
55
+ pry-byebug (3.6.0)
56
+ byebug (~> 10.0)
57
+ pry (~> 0.10)
58
+ rack (2.0.6)
59
+ rack-test (1.1.0)
60
+ rack (>= 1.0, < 3)
61
+ rails-dom-testing (2.0.3)
62
+ activesupport (>= 4.2.0)
63
+ nokogiri (>= 1.6)
64
+ rails-html-sanitizer (1.0.4)
65
+ loofah (~> 2.2, >= 2.2.2)
66
+ railties (5.2.2)
67
+ actionpack (= 5.2.2)
68
+ activesupport (= 5.2.2)
69
+ method_source
70
+ rake (>= 0.8.7)
71
+ thor (>= 0.19.0, < 2.0)
72
+ rainbow (3.0.0)
73
+ rake (10.5.0)
74
+ rspec (3.8.0)
75
+ rspec-core (~> 3.8.0)
76
+ rspec-expectations (~> 3.8.0)
77
+ rspec-mocks (~> 3.8.0)
78
+ rspec-core (3.8.0)
79
+ rspec-support (~> 3.8.0)
80
+ rspec-expectations (3.8.2)
81
+ diff-lcs (>= 1.2.0, < 2.0)
82
+ rspec-support (~> 3.8.0)
83
+ rspec-mocks (3.8.0)
84
+ diff-lcs (>= 1.2.0, < 2.0)
85
+ rspec-support (~> 3.8.0)
86
+ rspec-rails (3.8.1)
87
+ actionpack (>= 3.0)
88
+ activesupport (>= 3.0)
89
+ railties (>= 3.0)
90
+ rspec-core (~> 3.8.0)
91
+ rspec-expectations (~> 3.8.0)
92
+ rspec-mocks (~> 3.8.0)
93
+ rspec-support (~> 3.8.0)
94
+ rspec-support (3.8.0)
95
+ rubocop (0.62.0)
96
+ jaro_winkler (~> 1.5.1)
97
+ parallel (~> 1.10)
98
+ parser (>= 2.5, != 2.5.1.1)
99
+ powerpack (~> 0.1)
100
+ rainbow (>= 2.2.2, < 4.0)
101
+ ruby-progressbar (~> 1.7)
102
+ unicode-display_width (~> 1.4.0)
103
+ rubocop-rspec (1.31.0)
104
+ rubocop (>= 0.60.0)
105
+ ruby-progressbar (1.10.0)
106
+ simplecov (0.16.1)
107
+ docile (~> 1.1)
108
+ json (>= 1.8, < 3)
109
+ simplecov-html (~> 0.10.0)
110
+ simplecov-html (0.10.2)
111
+ standard (0.0.24)
112
+ rubocop (>= 0.60)
113
+ thor (0.20.3)
114
+ thread_safe (0.3.6)
115
+ tzinfo (1.2.5)
116
+ thread_safe (~> 0.1)
117
+ unicode-display_width (1.4.1)
118
+
119
+ PLATFORMS
120
+ ruby
121
+
122
+ DEPENDENCIES
123
+ actionpack (~> 5.2)
124
+ actionview (~> 5.2)
125
+ bundler (~> 1.16)
126
+ pry-byebug
127
+ rake (~> 10.0)
128
+ rspec (~> 3.0)
129
+ rspec-rails
130
+ rubanok!
131
+ rubocop-rspec
132
+ simplecov
133
+ standard
134
+
135
+ BUNDLED WITH
136
+ 1.17.2
data/README.md CHANGED
@@ -1,38 +1,206 @@
1
+ [![Gem Version](https://badge.fury.io/rb/rubanok.svg)](https://rubygems.org/gems/rubanok) [![Build Status](https://travis-ci.org/palkan/rubanok.svg?branch=master)](https://travis-ci.org/palkan/rubanok)
2
+
1
3
  # Rubanok
2
4
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rubanok`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ Rubanok provides a DSL to build parameters-based data transformers.
4
6
 
5
- TODO: Delete this and the text above, and describe your gem
7
+ The typical usage is to describe all the possible collection manipulation for REST `index` action, e.g. filtering, sorting, searching, pagination, etc..
6
8
 
7
- ## Installation
9
+ So, instead of:
8
10
 
9
- Add this line to your application's Gemfile:
11
+ ```ruby
12
+ class CourseSessionController < ApplicationController
13
+ def index
14
+ @sessions = CourseSession.
15
+ search(params[:q]).
16
+ by_course_type(params[:course_type_id]).
17
+ by_role(params[:role_id]).
18
+ paginate(page_params).
19
+ order(ordering_params)
20
+ end
21
+ end
22
+ ```
23
+
24
+ You have:
10
25
 
11
26
  ```ruby
12
- gem 'rubanok'
27
+ class CourseSessionController < ApplicationController
28
+ def index
29
+ @sessions = planish(
30
+ # pass input
31
+ CourseSession.all,
32
+ # pass params
33
+ params,
34
+ # provide a plane to use
35
+ with: CourseSessionsPlane
36
+ )
37
+ end
38
+ end
13
39
  ```
14
40
 
15
- And then execute:
41
+ Or we can try to infer all the configuration for you:
16
42
 
17
- $ bundle
18
43
 
19
- Or install it yourself as:
44
+ ```ruby
45
+ class CourseSessionController < ApplicationController
46
+ def index
47
+ @sessions = planish(CourseSession.all)
48
+ end
49
+ end
50
+ ```
20
51
 
21
- $ gem install rubanok
52
+ Requirements:
53
+ - Ruby ~> 2.5
54
+ - Rails >= 4.2 (only for using with Rails)
55
+
56
+ <a href="https://evilmartians.com/">
57
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
58
+
59
+ ## Installation
60
+
61
+ **This gem hasn't been released (and even built) yet.**
22
62
 
23
63
  ## Usage
24
64
 
25
- TODO: Write usage instructions here
65
+ The core concept of this library is a _plane_ (or _hand plane_, or "рубанок" in Russian). Plane is responsible for mapping parameters to transformrations.
66
+
67
+ From the example above:
68
+
69
+ ```ruby
70
+ class CourseSessionsPlane < Rubanok::Plane
71
+ # You can map keys
72
+ map :q do |q:|
73
+ # `raw` is an accessor for input data
74
+ raw.searh(q)
75
+ end
76
+ end
77
+
78
+ # The following code
79
+ CourseSessionsPlane.call(CourseSession.all, q: "xyz")
80
+
81
+ # is equal to
82
+ CourseSession.all.search("xyz")
83
+ ```
84
+
85
+ You can map multiple keys at once:
86
+
87
+ ```ruby
88
+ class CourseSessionsPlane < Rubanok::Plane
89
+ DEFAULT_PAGE_SIZE = 25
90
+
91
+ map :page, :per_page do |page:, per_page: DEFAULT_PAGE_SIZE|
92
+ raw.paginate(page: page, per_page: per_page)
93
+ end
94
+ end
95
+ ```
96
+
97
+ There is also `match` method to handle values:
98
+
99
+ ```ruby
100
+ class CourseSessionsPlane < Rubanok::Plane
101
+ SORT_ORDERS = %w(asc desc).freeze
102
+ SORTABLE_FIELDS = %w(id name created_at).freeze
103
+
104
+ match :sort_by, :sort do
105
+ having "course_id", "desc" do
106
+ raw.joins(:courses).order("courses.id desc nulls last")
107
+ end
108
+
109
+ having "course_id", "asc" do
110
+ raw.joins(:courses).order("courses.id asc nulls first")
111
+ end
112
+
113
+ # Match any value for the second arg
114
+ having "type" do |sort: "asc"|
115
+ # Prevent SQL injections
116
+ raise "Possible injection: #{sort}" unless SORT_ORDERS.include?(sort)
117
+ raw.joins(:course_type).order("course_types.name #{sort}")
118
+ end
119
+
120
+ # Match any value
121
+ default do |sort_by:, sort: "asc"|
122
+ raise "Possible injection: #{sort}" unless SORT_ORDERS.include?(sort)
123
+ raise "The field is not sortable: #{sort_by}" unless SORTABLE_FIELDS.include?(sort_by)
124
+ raw.order(sort_by => sort)
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ **NOTE:** matching only match the exact values; more complex matching could be added in the future.
131
+
132
+ ### Rule activation
133
+
134
+ Rubanok _activates_ a rule by checking whether the corresponding keys are present in the params object. All the fields must be present to apply the rule.
135
+
136
+ Sometimes you might want to make some fields optional (or event all of them). You can use `activate_on` and `activate_always` options for that:
137
+
138
+ ```ruby
139
+ # Always apply the rule; use default values for keyword args
140
+ map :page, :per_page, activate_always: true do |page: 1, per_page: 2|
141
+ raw.page(page).per(per_page)
142
+ end
143
+
144
+ # Only require `sort_by` to be preset to activate sorting rule
145
+ match :sort_by, :sort, activate_on: :sort_by do
146
+ # ...
147
+ end
148
+ ```
149
+
150
+ By default, Rubanok ignores empty param values (using `#empty?` under the hood) and do not activate the matching rules (i.e. `{ q: "" }` or `{ q: nil }` won't activate the `map :q` rule).
151
+
152
+ You can change this behaviour by setting: `Rubanok.ignore_empty_values = false`.
153
+
154
+ ### Testing
155
+
156
+ One of the benefits of having all the modification logic in its own class is the ability to test it in isolation:
157
+
158
+ ```ruby
159
+ # For example, with RSpec
160
+ describe CourseSessionsPlane do
161
+ let(:input ) { CourseSession.all }
162
+ let(:params) { {} }
163
+
164
+ subject { described_class.call(input, params) }
165
+
166
+ specify "searching" do
167
+ params[:q] = "wood"
168
+
169
+ expect(subject).to eq input.search("wood")
170
+ end
171
+ end
172
+ ```
173
+
174
+ Now in your controller you only have to test that the specific _plane_ is applied:
175
+
176
+ ```ruby
177
+ describe CourseSessionController do
178
+ subject { get :index }
179
+
180
+ specify do
181
+ expect { subject }.to have_planished(CourseSession.all).
182
+ with(CourseSessionsPlane)
183
+ end
184
+ end
185
+ ```
186
+
187
+ **NOTE**: input matching only checks for the class equality.
188
+
189
+ To use `have_planished` matcher you must add the following line to your `spec_helper.rb` / `rails_helper.rb` (it's added automatically if RSpec defined and `RAILS_ENV`/`RACK_ENV` is equal to `"test"`):
190
+
191
+ ```ruby
192
+ require "rubanok/rspec"
193
+ ```
26
194
 
27
- ## Development
195
+ ### Rails vs. non-Rails
28
196
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
197
+ Rubanok is a Rails-free library but has some useful Rails extensions, such as `planish` helper for controllers (included automatically into `ActionController::Base` and `ActionController::API`).
30
198
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
199
+ If you use `ActionController::Metal` you must include the `Rubanok::Controller` module yourself.
32
200
 
33
201
  ## Contributing
34
202
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rubanok.
203
+ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/rubanok.
36
204
 
37
205
  ## License
38
206
 
data/Rakefile CHANGED
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
3
6
 
4
7
  RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
5
9
 
6
- task :default => :spec
10
+ task default: [:rubocop, :spec]
@@ -2,13 +2,7 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "rubanok"
5
+ require "pry"
5
6
 
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(__FILE__)
7
+ require "pry"
8
+ Pry.start
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "actionpack", "~> 4.2"
4
+ gem "actionview", "~> 4.2"
5
+
6
+ gemspec path: ".."
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "actionpack", "~> 5.2"
4
+ gem "actionview", "~> 5.2"
5
+
6
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", github: "rails/rails"
4
+
5
+ gemspec path: ".."
@@ -1,5 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rubanok/version"
4
+ require "rubanok/plane"
5
+
6
+ require "rubanok/railtie" if defined?(Rails)
2
7
 
8
+ if defined?(RSpec) && (ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test")
9
+ require "rubanok/rspec"
10
+ end
11
+
12
+ # Rubanok provides a DSL to build parameters-based data transformers.
13
+ #
14
+ # Example:
15
+ #
16
+ # class CourseSessionPlane < Rubanok::Plane
17
+ # map :q do |q:|
18
+ # raw.searh(q)
19
+ # end
20
+ # end
21
+ #
22
+ # class CourseSessionController < ApplicationController
23
+ # def index
24
+ # @sessions = planish(CourseSession.all)
25
+ # end
26
+ # end
3
27
  module Rubanok
4
- # Your code goes here...
28
+ class << self
29
+ # Define whether to ignore empty values in params or not.
30
+ # When the value is empty and ignored the corresponding matcher/mapper
31
+ # is not activated (true by default)
32
+ attr_accessor :ignore_empty_values
33
+ end
34
+
35
+ self.ignore_empty_values = true
5
36
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubanok
4
+ module DSL
5
+ # Adds `.map` method to Plane to define key-matching rules:
6
+ #
7
+ # map :q do |q:|
8
+ # # this rule is activated iff "q" (or :q) param is present
9
+ # # the value is passed to the handler
10
+ # end
11
+ module Mapping
12
+ class Rule < Rubanok::Rule
13
+ METHOD_PREFIX = "__map"
14
+
15
+ private
16
+
17
+ # prefix rule method name to avoid collisions
18
+ def build_method_name
19
+ "#{METHOD_PREFIX}#{super}"
20
+ end
21
+ end
22
+
23
+ def map(*fields, **options, &block)
24
+ rule = Rule.new(fields, options)
25
+
26
+ define_method(rule.to_method_name, &block)
27
+
28
+ rules << rule
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubanok
4
+ module DSL
5
+ # Adds `.match` method to Plane class to define key-value-matching rules:
6
+ #
7
+ # match :sort, :sort_by do |sort:, sort_by:|
8
+ # # this rule is activated iff both "sort" and "sort_by" params are present
9
+ # # the values are passed to the matcher
10
+ # #
11
+ # # then we match against values
12
+ # having "name" do |sort_by:|
13
+ # raw.joins(:user).order("users.name #{sort_by}")
14
+ # end
15
+ # end
16
+ module Matching
17
+ class Rule < Rubanok::Rule
18
+ METHOD_PREFIX = "__match"
19
+
20
+ class Clause < Rubanok::Rule
21
+ attr_reader :values, :id, :block
22
+
23
+ def initialize(id, fields, values = [], **options, &block)
24
+ super(fields, options)
25
+ @id = id
26
+ @block = block
27
+ @values = Hash[fields.take(values.size).zip(values)].freeze
28
+ @fields = (fields - @values.keys).freeze
29
+ end
30
+
31
+ def applicable?(params)
32
+ values.all? { |key, matcher| params.key?(key) && (matcher == params[key]) }
33
+ end
34
+
35
+ alias to_method_name id
36
+ end
37
+
38
+ attr_reader :clauses
39
+
40
+ def initialize(*)
41
+ super
42
+ @clauses = []
43
+ end
44
+
45
+ def matching_clause(params)
46
+ clauses.detect do |clause|
47
+ clause.applicable?(params)
48
+ end
49
+ end
50
+
51
+ def having(*values, &block)
52
+ clauses << Clause.new("#{to_method_name}_#{clauses.size}", fields, values, &block)
53
+ end
54
+
55
+ def default(&block)
56
+ clauses << Clause.new("#{to_method_name}_default", fields, activate_always: true, &block)
57
+ end
58
+
59
+ private
60
+
61
+ # prefix rule method name to avoid collisions
62
+ def build_method_name
63
+ "#{METHOD_PREFIX}#{super}"
64
+ end
65
+ end
66
+
67
+ def match(*fields, **options, &block)
68
+ rule = Rule.new(fields, options)
69
+
70
+ rule.instance_eval(&block)
71
+
72
+ define_method(rule.to_method_name) do |params|
73
+ clause = rule.matching_clause(params)
74
+ next raw unless clause
75
+
76
+ apply_rule! clause.to_method_name, clause.project(params)
77
+ end
78
+
79
+ rule.clauses.each do |clause|
80
+ define_method(clause.to_method_name, &clause.block)
81
+ end
82
+
83
+ rules << rule
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubanok
4
+ module SymbolizeKeys
5
+ refine Hash do
6
+ def symbolize_keys
7
+ each_with_object({}) do |(key, value), acc|
8
+ acc[key.to_sym] = value
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubanok/rule"
4
+
5
+ require "rubanok/dsl/mapping"
6
+ require "rubanok/dsl/matching"
7
+
8
+ require "rubanok/ext/symbolize_keys"
9
+ using Rubanok::SymbolizeKeys
10
+
11
+ module Rubanok
12
+ # Base class for transformers (_planes_)
13
+ #
14
+ # Define transformation rules via `map` and `match` methods
15
+ # and apply them by calling the plane:
16
+ #
17
+ # class MyPlane < Rubanok::Plane
18
+ # map :type do
19
+ # raw.where(type: type)
20
+ # end
21
+ # end
22
+ #
23
+ # MyPlane.call(MyModel.all, {type: "public"})
24
+ #
25
+ # NOTE: the second argument (`params`) MUST be a Hash. Keys could be either Symbols
26
+ # or Strings (we automatically transform strings to symbols while matching rules).
27
+ #
28
+ # All transformation methods are called within the context of the instance of
29
+ # a plane class.
30
+ #
31
+ # You can access the input data via `raw` method.
32
+ class Plane
33
+ class << self
34
+ include DSL::Mapping
35
+ include DSL::Matching
36
+
37
+ def call(input, params)
38
+ new(input).call(params)
39
+ end
40
+
41
+ def add_rule(rule)
42
+ rules << rule
43
+ end
44
+
45
+ def rules
46
+ return @rules if instance_variable_defined?(:@rules)
47
+
48
+ @rules =
49
+ if superclass <= Plane
50
+ superclass.rules.dup
51
+ else
52
+ []
53
+ end
54
+ end
55
+ end
56
+
57
+ def initialize(input)
58
+ @input = input
59
+ end
60
+
61
+ def call(params)
62
+ params = params.symbolize_keys
63
+
64
+ rules.each do |rule|
65
+ next unless rule.applicable?(params)
66
+
67
+ apply_rule! rule.to_method_name, rule.project(params)
68
+ end
69
+
70
+ input
71
+ end
72
+
73
+ private
74
+
75
+ attr_accessor :input
76
+
77
+ alias raw input
78
+
79
+ def apply_rule!(method_name, data)
80
+ self.input =
81
+ if data.empty?
82
+ send(method_name)
83
+ else
84
+ send(method_name, **data)
85
+ end
86
+ end
87
+
88
+ def rules
89
+ self.class.rules
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Rubanok
6
+ # Controller concern.
7
+ # Adds `planish` method.
8
+ module Controller
9
+ extend ActiveSupport::Concern
10
+
11
+ # Planish passed data (e.g. ActiveRecord relation) using
12
+ # the corrsponding Plane class.
13
+ #
14
+ # Plane is inferred from controller name, e.g.
15
+ # "PostsController -> PostPlane".
16
+ #
17
+ # You can specify the Plane class explicitly via `with` option.
18
+ #
19
+ # By default, `params` object is passed as paraters, but you
20
+ # can specify the params via `params` option.
21
+ def planish(data, plane_params = nil, with: implicit_plane_class)
22
+ if with.nil?
23
+ raise ArgumentError, "Failed to find a plane class for #{self.class.name}. " \
24
+ "Please, specify the plane class explicitly via `with` option"
25
+ end
26
+
27
+ plane_params ||= params
28
+
29
+ plane_params = plane_params.to_unsafe_h if plane_params.is_a?(ActionController::Parameters)
30
+ with.call(data, plane_params)
31
+ end
32
+
33
+ # Tries to infer the plane class from controller path
34
+ def implicit_plane_class
35
+ "#{controller_path.classify.pluralize}Plane".safe_constantize
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubanok # :nodoc:
4
+ class Railtie < ::Rails::Railtie # :nodoc:
5
+ config.to_prepare do |_app|
6
+ ActiveSupport.on_load(:action_controller) do
7
+ require "rubanok/rails/controller"
8
+
9
+ ActionController::Base.include Rubanok::Controller
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/mocks"
4
+
5
+ module Rubanok
6
+ class HavePlanished < RSpec::Matchers::BuiltIn::BaseMatcher
7
+ include RSpec::Mocks::ExampleMethods
8
+
9
+ attr_reader :data_class, :plane, :matcher
10
+
11
+ def initialize(data_class = nil)
12
+ if data_class
13
+ @data_class = data_class.is_a?(Module) ? data_class : data_class.class
14
+ end
15
+ @matcher = have_received(:call)
16
+ end
17
+
18
+ def with(plane)
19
+ @plane = plane
20
+ self
21
+ end
22
+
23
+ def supports_block_expectations?
24
+ true
25
+ end
26
+
27
+ def matches?(proc)
28
+ raise ArgumentError, "have_planished only supports block expectations" unless Proc === proc
29
+
30
+ raise ArgumentError, "Plane class is required. Please, specify it using `.with` modifier" if plane.nil?
31
+
32
+ allow(plane).to receive(:call)
33
+ proc.call
34
+
35
+ matcher.with(an_instance_of(data_class), anything) if data_class
36
+
37
+ matcher.matches?(plane)
38
+ end
39
+
40
+ def failure_message
41
+ "expected to use #{plane.name}#{data_class ? " for #{data_class.name}" : ""}, but didn't"
42
+ end
43
+
44
+ def failure_message_when_negated
45
+ "expected not to use #{plane.name}#{data_class ? " for #{data_class.name} " : ""}, but have used"
46
+ end
47
+ end
48
+ end
49
+
50
+ RSpec.configure do |config|
51
+ config.include(Module.new do
52
+ def have_planished(*args)
53
+ Rubanok::HavePlanished.new(*args)
54
+ end
55
+ end)
56
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubanok
4
+ class Rule # :nodoc:
5
+ UNDEFINED = :__undef__
6
+
7
+ attr_reader :fields, :activate_on, :activate_always
8
+
9
+ def initialize(fields, activate_on: fields, activate_always: false)
10
+ @fields = fields.freeze
11
+ @activate_on = Array(activate_on).freeze
12
+ @activate_always = activate_always
13
+ end
14
+
15
+ def project(params)
16
+ fields.each_with_object({}) do |field, acc|
17
+ val = fetch_value params, field
18
+ next acc if val == UNDEFINED
19
+
20
+ acc[field] = val
21
+ acc
22
+ end
23
+ end
24
+
25
+ def applicable?(params)
26
+ return true if activate_always == true
27
+
28
+ activate_on.all? { |field| params.key?(field) && !empty?(params[field]) }
29
+ end
30
+
31
+ def to_method_name
32
+ @method_name ||= build_method_name
33
+ end
34
+
35
+ private
36
+
37
+ def build_method_name
38
+ "__#{fields.join("_")}__"
39
+ end
40
+
41
+ def fetch_value(params, field)
42
+ return UNDEFINED if !params.key?(field) || empty?(params[field])
43
+
44
+ params[field]
45
+ end
46
+
47
+ using(Module.new do
48
+ refine NilClass do
49
+ def empty?
50
+ true
51
+ end
52
+ end
53
+
54
+ refine Object do
55
+ def empty?
56
+ false
57
+ end
58
+ end
59
+ end)
60
+
61
+ def empty?(val)
62
+ return false unless Rubanok.ignore_empty_values
63
+
64
+ val.empty?
65
+ end
66
+ end
67
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rubanok
2
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
3
5
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -13,16 +14,22 @@ Gem::Specification.new do |spec|
13
14
  spec.description = "Parameters-based transformation DSL"
14
15
  spec.homepage = "https://github.com/palkan/rubanok"
15
16
  spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 2.5.0"
16
18
 
17
19
  # Specify which files should be added to the gem when it is released.
18
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
20
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
23
  end
22
24
 
23
25
  spec.require_paths = ["lib"]
24
26
 
27
+ spec.add_development_dependency "actionpack", ">= 4.2"
28
+ spec.add_development_dependency "actionview", ">= 4.2"
25
29
  spec.add_development_dependency "bundler", "~> 1.16"
26
30
  spec.add_development_dependency "rake", "~> 10.0"
27
31
  spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "rspec-rails"
33
+ spec.add_development_dependency "rubocop-rspec"
34
+ spec.add_development_dependency "standard"
28
35
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubanok
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-07 00:00:00.000000000 Z
11
+ date: 2019-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: actionview
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +80,48 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: standard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
55
125
  description: Parameters-based transformation DSL
56
126
  email:
57
127
  - dementiev.vm@gmail.com
@@ -61,14 +131,29 @@ extra_rdoc_files: []
61
131
  files:
62
132
  - ".gitignore"
63
133
  - ".rspec"
134
+ - ".rubocop.yml"
64
135
  - ".travis.yml"
136
+ - CHANGELOG.md
65
137
  - Gemfile
138
+ - Gemfile.local
139
+ - Gemfile.lock
66
140
  - LICENSE.txt
67
141
  - README.md
68
142
  - Rakefile
69
143
  - bin/console
70
144
  - bin/setup
145
+ - gemfiles/rails42.gemfile
146
+ - gemfiles/rails52.gemfile
147
+ - gemfiles/railsmaster.gemfile
71
148
  - lib/rubanok.rb
149
+ - lib/rubanok/dsl/mapping.rb
150
+ - lib/rubanok/dsl/matching.rb
151
+ - lib/rubanok/ext/symbolize_keys.rb
152
+ - lib/rubanok/plane.rb
153
+ - lib/rubanok/rails/controller.rb
154
+ - lib/rubanok/railtie.rb
155
+ - lib/rubanok/rspec.rb
156
+ - lib/rubanok/rule.rb
72
157
  - lib/rubanok/version.rb
73
158
  - rubanok.gemspec
74
159
  homepage: https://github.com/palkan/rubanok
@@ -83,15 +168,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
168
  requirements:
84
169
  - - ">="
85
170
  - !ruby/object:Gem::Version
86
- version: '0'
171
+ version: 2.5.0
87
172
  required_rubygems_version: !ruby/object:Gem::Requirement
88
173
  requirements:
89
174
  - - ">="
90
175
  - !ruby/object:Gem::Version
91
176
  version: '0'
92
177
  requirements: []
93
- rubyforge_project:
94
- rubygems_version: 2.7.7
178
+ rubygems_version: 3.0.1
95
179
  signing_key:
96
180
  specification_version: 4
97
181
  summary: Parameters-based transformation DSL