rubanok 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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