diver_down 0.0.1.alpha2 → 0.0.1.alpha4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +54 -18
- data/lib/diver_down/helper.rb +10 -14
- data/lib/diver_down/trace/call_stack.rb +23 -8
- data/lib/diver_down/trace/session.rb +147 -0
- data/lib/diver_down/trace/tracer.rb +7 -13
- data/lib/diver_down/trace.rb +1 -1
- data/lib/diver_down/version.rb +1 -1
- data/lib/diver_down/web/action.rb +3 -2
- data/lib/diver_down/web/definition_enumerator.rb +7 -1
- data/lib/diver_down/web.rb +2 -1
- data/web/assets/bundle.js +47 -45
- metadata +10 -6
- data/lib/diver_down/trace/tracer/session.rb +0 -121
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fda9d1924b7ace3a55962178519fc291dc96c1fc99cf44fa58f4aa0b4a41232
|
4
|
+
data.tar.gz: e2e693488036833f19b8cd92d7c122bd16a2363cbb9b6dbcb4463b9fb0e595d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f868ba19a8a7d175ab8016219dfac382fbaa47dd7c1e5629928d2c8bb3fbd8ee29d99ea307a296d24b5f5b9caf91aaa4c9f11ca404771d331ef776b42afad54
|
7
|
+
data.tar.gz: 9082d6d8e245e5d806f1a5fedeee01131d7b499ec4a2b61a904ecf083d2582999b153c9112503e4b46e091a4cbdf07ca51dade5b47845870607d417d0cb107d1
|
data/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# DiverDown
|
2
2
|
|
3
|
-
`
|
4
|
-
This tool was created to analyze Ruby applications for use in large-scale refactoring such as moduler monolith.
|
3
|
+
`DiverDown` is a tool designed to dynamically analyze application dependencies and generate a comprehensive dependency map. It is particularly useful for analyzing Ruby applications, aiding significantly in large-scale refactoring projects or transitions towards a modular monolith architecture.
|
5
4
|
|
6
|
-
|
5
|
+
## Features
|
7
6
|
|
8
|
-
|
7
|
+
- **Dependency Mapping**: Analyze and generate an application dependencies.
|
8
|
+
- **Module Categorization**: Organizes file-by-file associations into specific groups, facilitating deeper modular monolith architectural analysis and understanding.
|
9
|
+
|
10
|
+
## Getting Started
|
9
11
|
|
10
12
|
Add this line to your application's Gemfile:
|
11
13
|
|
@@ -27,7 +29,7 @@ Or install it yourself as:
|
|
27
29
|
|
28
30
|
### `DiverDown::Trace`
|
29
31
|
|
30
|
-
|
32
|
+
The `DiverDown::Trace` module analyzes the execution of Ruby code and outputs the results as `DiverDown::Definition` objects.
|
31
33
|
|
32
34
|
```ruby
|
33
35
|
tracer = DiverDown::Trace::Tracer.new
|
@@ -57,8 +59,49 @@ tracer.trace(title: 'title', definition_group: 'group name') do
|
|
57
59
|
end
|
58
60
|
```
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
+
**Options**
|
63
|
+
|
64
|
+
When analyzing user applications, it is recommended to specify the option.
|
65
|
+
|
66
|
+
|name|type|description|example|default|
|
67
|
+
| --- | --- | --- | --- | --- |
|
68
|
+
| `module_set` | Hash{ <br> modules: Array<Module, String> \| Set<Module, String> \| nil,<br> paths: Array\<String> \| Set\<String> \| nil <br>}<br>\| DiverDown::Trace::ModuleSet | Specify the class/module to be included in the analysis results.<br><br>If you know the module name:<br>`{ modules: [ModA, ModB] }`<br><br>If you know module path:<br>`{ paths: ['/path/to/rails/app/models/mod_a.rb'] }` | `{ paths: Dir["app/**/*.rb"] }` | `nil`. All class/modul are target. |
|
69
|
+
| `caller_paths` | `Array<String> \| nil` | Specifies a list of allowed paths as caller paths. By specifying the user application path in this list of paths and excluding paths under the gem, the caller path is identified back to the user application path. | `Dir["app/**/*.rb"]` | `nil`. All paths are target. |
|
70
|
+
| `filter_method_id_path` | `#call \| nil` | lambda to convert the caller path. | `->(path) { path.remove(Rails.root) }` | `nil`. No conversion. |
|
71
|
+
|
72
|
+
**Example**
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
# Your rails application paths
|
76
|
+
application_paths = [
|
77
|
+
*Dir['app/**/*.rb'],
|
78
|
+
*Dir['lib/**/*.rb'],
|
79
|
+
].map { File.expand_path(_1) }
|
80
|
+
|
81
|
+
ignored_application_paths = [
|
82
|
+
'app/models/application_record.rb',
|
83
|
+
].map { File.expand_path(_1) }
|
84
|
+
|
85
|
+
module_set = DiverDown::Trace::ModuleSet.new(modules: modules - ignored_modules)
|
86
|
+
|
87
|
+
filter_method_id_path = ->(path) { path.remove("#{Rails.root}/") }
|
88
|
+
|
89
|
+
tracer = DiverDown::Trace::Tracer.new(
|
90
|
+
caller_paths: application_paths,
|
91
|
+
module_set: {
|
92
|
+
paths: (application_paths - ignored_application_paths)
|
93
|
+
},
|
94
|
+
filter_method_id_path:
|
95
|
+
)
|
96
|
+
|
97
|
+
definition = tracer.trace do
|
98
|
+
# do something
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Output Results
|
103
|
+
|
104
|
+
The analysis results are intended to be saved to a specific directory in either `.json` or `.yaml` format. These files are compatible with `DiverDown::Web`, which can read and display the results.
|
62
105
|
|
63
106
|
```ruby
|
64
107
|
dir = 'tmp/diver_down'
|
@@ -71,19 +114,14 @@ File.write(File.join(dir, "#{definition.title}.json"), definition.to_h.to_json)
|
|
71
114
|
File.write(File.join(dir, "#{definition.title}.yaml"), definition.to_h.to_yaml)
|
72
115
|
```
|
73
116
|
|
74
|
-
**Options**
|
75
|
-
|
76
|
-
TODO
|
77
|
-
|
78
117
|
### `DiverDown::Web`
|
79
118
|
|
80
119
|
View the analysis results in a browser.
|
81
120
|
|
82
|
-
This gem is designed to
|
83
|
-
Each file in the analysis can be specified to belong to a module you specify on the browser.
|
121
|
+
This gem is specifically designed to analyze large applications with a modular monolithic architecture. It allows users to categorize each analyzed file into specified modules directly through the web interface.
|
84
122
|
|
85
|
-
- `--definition-dir`
|
86
|
-
- `--module-store-path`
|
123
|
+
- `--definition-dir` Specifies the directory where the analysis results are stored.
|
124
|
+
- `--module-store-path` Designates a path to save the results that include details on which module each file belongs to. If this option is not specified, the results will be temporarily stored in a default temporary file.
|
87
125
|
|
88
126
|
```sh
|
89
127
|
bundle exec diver_down_web --definition-dir tmp/diver_down --module-store-path tmp/module_store.yml
|
@@ -102,7 +140,7 @@ open http://localhost:8080
|
|
102
140
|
- Ruby: `bundle exec rspec`, `bundle exec rubocop`
|
103
141
|
- TypeScript: `pnpm run test`, `pnpm run lint`
|
104
142
|
|
105
|
-
### Development DiverDown::Web
|
143
|
+
### Development `DiverDown::Web`
|
106
144
|
|
107
145
|
If you want to develop `DiverDown::Web` locally, set up a server for development.
|
108
146
|
|
@@ -113,8 +151,6 @@ $ pnpm run dev
|
|
113
151
|
|
114
152
|
# Start server for backend
|
115
153
|
$ bundle install
|
116
|
-
# DIVER_DOWN_DIR specifies the directory where the analysis results are stored.
|
117
|
-
# DIVER_DOWN_MODULE_STORE specifies a yaml file that defines which module the file belongs to, but this file is newly created, so it works even if the file does not exist.
|
118
154
|
$ DIVER_DOWN_DIR=/path/to/definitions_dir DIVER_DOWN_MODULE_STORE=/path/to/module_store.yml bundle exec puma
|
119
155
|
```
|
120
156
|
|
data/lib/diver_down/helper.rb
CHANGED
@@ -28,7 +28,11 @@ module DiverDown
|
|
28
28
|
# @return [Module, Class]
|
29
29
|
def self.resolve_module(obj)
|
30
30
|
if module?(obj) # Do not call method of this
|
31
|
-
|
31
|
+
if module_subclass?(obj)
|
32
|
+
obj.class
|
33
|
+
else
|
34
|
+
resolve_singleton_class(obj)
|
35
|
+
end
|
32
36
|
else
|
33
37
|
k = INSTANCE_CLASS_QUERY.bind_call(obj)
|
34
38
|
resolve_singleton_class(k)
|
@@ -63,19 +67,11 @@ module DiverDown
|
|
63
67
|
::ActiveSupport::Inflector.constantize(str)
|
64
68
|
end
|
65
69
|
|
66
|
-
#
|
67
|
-
# @return [
|
68
|
-
def self.
|
69
|
-
|
70
|
-
|
71
|
-
object.each_with_object({}) do |(key, value), memo|
|
72
|
-
memo[key.to_sym] = deep_symbolize_keys(value)
|
73
|
-
end
|
74
|
-
when Array
|
75
|
-
object.map { deep_symbolize_keys(_1) }
|
76
|
-
else
|
77
|
-
object
|
78
|
-
end
|
70
|
+
# FIXME: I don't know the best way to determine which class inherits from Module.
|
71
|
+
# @return [Boolean]
|
72
|
+
def self.module_subclass?(mod)
|
73
|
+
mod.ancestors.size == 1 &&
|
74
|
+
mod.class < Module
|
79
75
|
end
|
80
76
|
end
|
81
77
|
end
|
@@ -12,32 +12,47 @@ module DiverDown
|
|
12
12
|
attr_reader :stack_size
|
13
13
|
|
14
14
|
def initialize
|
15
|
+
@ignored_stack_size = nil
|
15
16
|
@stack_size = 0
|
16
|
-
@
|
17
|
+
@context_stack = {}
|
17
18
|
end
|
18
19
|
|
19
20
|
# @return [Boolean]
|
20
|
-
def
|
21
|
-
@
|
21
|
+
def empty_context_stack?
|
22
|
+
@context_stack.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Array<Integer>]
|
26
|
+
def context_stack_size
|
27
|
+
@context_stack.keys
|
22
28
|
end
|
23
29
|
|
24
30
|
# @return [Array<Object>]
|
25
|
-
def
|
26
|
-
@
|
31
|
+
def context_stack
|
32
|
+
@context_stack.values
|
27
33
|
end
|
28
34
|
|
29
35
|
# @param context [Object, nil] User defined stack context.
|
30
36
|
# @return [void]
|
31
|
-
def push(context = nil)
|
37
|
+
def push(context = nil, ignored: false)
|
32
38
|
@stack_size += 1
|
33
|
-
@
|
39
|
+
@context_stack[@stack_size] = context unless context.nil?
|
40
|
+
@ignored_stack_size ||= @stack_size if ignored
|
41
|
+
end
|
42
|
+
|
43
|
+
# If a stack is not already a tracing target, some conditions are unnecessary so that it can be easily ignored.
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
def ignored?
|
47
|
+
!@ignored_stack_size.nil?
|
34
48
|
end
|
35
49
|
|
36
50
|
# @return [void]
|
37
51
|
def pop
|
38
52
|
raise StackEmptyError if @stack_size.zero?
|
39
53
|
|
40
|
-
@
|
54
|
+
@context_stack.delete(@stack_size) if @context_stack.key?(@stack_size)
|
55
|
+
@ignored_stack_size = nil if @ignored_stack_size && @ignored_stack_size == @stack_size
|
41
56
|
@stack_size -= 1
|
42
57
|
end
|
43
58
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DiverDown
|
4
|
+
module Trace
|
5
|
+
class Session
|
6
|
+
StackContext = Data.define(
|
7
|
+
:source,
|
8
|
+
:method_id,
|
9
|
+
:path,
|
10
|
+
:lineno
|
11
|
+
)
|
12
|
+
|
13
|
+
attr_reader :definition
|
14
|
+
|
15
|
+
# @param [DiverDown::Trace::ModuleSet, nil] module_set
|
16
|
+
# @param [DiverDown::Trace::IgnoredMethodIds, nil] ignored_method_ids
|
17
|
+
# @param [Set<String>, nil] List of paths to finish traversing when searching for a caller. If nil, all paths are finished.
|
18
|
+
# @param [#call, nil] filter_method_id_path
|
19
|
+
def initialize(module_set: DiverDown::Trace::ModuleSet.new, ignored_method_ids: nil, caller_paths: nil, filter_method_id_path: nil, definition: DiverDown::Definition.new)
|
20
|
+
@module_set = module_set
|
21
|
+
@ignored_method_ids = ignored_method_ids
|
22
|
+
@caller_paths = caller_paths
|
23
|
+
@filter_method_id_path = filter_method_id_path
|
24
|
+
@definition = definition
|
25
|
+
@trace_point = build_trace_point
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [void]
|
29
|
+
def start
|
30
|
+
@trace_point.enable
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [void]
|
34
|
+
def stop
|
35
|
+
@trace_point.disable
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_trace_point
|
41
|
+
call_stack = DiverDown::Trace::CallStack.new
|
42
|
+
|
43
|
+
TracePoint.new(*DiverDown::Trace::Tracer.trace_events) do |tp|
|
44
|
+
# Skip the trace of the library itself
|
45
|
+
next if tp.path&.start_with?(DiverDown::LIB_DIR)
|
46
|
+
next if TracePoint == tp.defined_class
|
47
|
+
|
48
|
+
case tp.event
|
49
|
+
when :call, :c_call
|
50
|
+
# puts "#{tp.method_id} #{tp.path}:#{tp.lineno}"
|
51
|
+
if call_stack.ignored?
|
52
|
+
call_stack.push
|
53
|
+
next
|
54
|
+
end
|
55
|
+
|
56
|
+
mod = DiverDown::Helper.resolve_module(tp.self)
|
57
|
+
|
58
|
+
unless mod
|
59
|
+
call_stack.push
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
if !@ignored_method_ids.nil? && @ignored_method_ids.ignored?(mod, DiverDown::Helper.module?(tp.self), tp.method_id)
|
64
|
+
# If this method is ignored, the call stack is ignored until the method returns.
|
65
|
+
call_stack.push(ignored: true)
|
66
|
+
next
|
67
|
+
end
|
68
|
+
|
69
|
+
source_name = DiverDown::Helper.normalize_module_name(mod) if @module_set.include?(mod)
|
70
|
+
pushed = false
|
71
|
+
|
72
|
+
unless source_name.nil?
|
73
|
+
# If the call stack contains a call to a module to be traced
|
74
|
+
# `@ignored_call_stack` is not nil means the call stack contains a call to a module to be ignored
|
75
|
+
unless call_stack.empty_context_stack?
|
76
|
+
# Add dependency to called source
|
77
|
+
called_stack_context = call_stack.context_stack[-1]
|
78
|
+
called_source = called_stack_context.source
|
79
|
+
dependency = called_source.find_or_build_dependency(source_name)
|
80
|
+
|
81
|
+
# `dependency.nil?` means source_name equals to called_source.source.
|
82
|
+
# self-references are not tracked because it is not "dependency".
|
83
|
+
if dependency
|
84
|
+
context = DiverDown::Helper.module?(tp.self) ? 'class' : 'instance'
|
85
|
+
method_id = dependency.find_or_build_method_id(name: tp.method_id, context:)
|
86
|
+
method_id_path = "#{called_stack_context.path}:#{called_stack_context.lineno}"
|
87
|
+
method_id_path = @filter_method_id_path.call(method_id_path) if @filter_method_id_path
|
88
|
+
method_id.add_path(method_id_path)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Search is a heavy process and should be terminated early.
|
93
|
+
# The position of the most recently found caller or the start of trace is used as the maximum value.
|
94
|
+
maximum_back_stack_size = if call_stack.empty_context_stack?
|
95
|
+
call_stack.stack_size
|
96
|
+
else
|
97
|
+
call_stack.stack_size - call_stack.context_stack_size[-1]
|
98
|
+
end
|
99
|
+
|
100
|
+
caller_location = find_neast_caller_location(maximum_back_stack_size)
|
101
|
+
|
102
|
+
# `caller_location` is nil if it is filtered by caller_paths
|
103
|
+
if caller_location
|
104
|
+
pushed = true
|
105
|
+
source = @definition.find_or_build_source(source_name)
|
106
|
+
|
107
|
+
call_stack.push(
|
108
|
+
StackContext.new(
|
109
|
+
source:,
|
110
|
+
method_id: tp.method_id,
|
111
|
+
path: caller_location.path,
|
112
|
+
lineno: caller_location.lineno
|
113
|
+
)
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
call_stack.push unless pushed
|
119
|
+
when :return, :c_return
|
120
|
+
call_stack.pop
|
121
|
+
end
|
122
|
+
rescue StandardError
|
123
|
+
tp.disable
|
124
|
+
raise
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def find_neast_caller_location(stack_size)
|
129
|
+
excluded_frame_size = 1 # Ignore neast caller because it is stack of #find_neast_caller_location.
|
130
|
+
finished_frame_size = excluded_frame_size + stack_size + 1
|
131
|
+
frame_pos = 1
|
132
|
+
|
133
|
+
# If @caller_paths is nil, return the caller location.
|
134
|
+
return caller_locations(excluded_frame_size + 1, excluded_frame_size + 1)[0] if @caller_paths.nil?
|
135
|
+
|
136
|
+
Thread.each_caller_location do
|
137
|
+
break if finished_frame_size < frame_pos
|
138
|
+
return _1 if @caller_paths.include?(_1.path)
|
139
|
+
|
140
|
+
frame_pos += 1
|
141
|
+
end
|
142
|
+
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -3,12 +3,6 @@
|
|
3
3
|
module DiverDown
|
4
4
|
module Trace
|
5
5
|
class Tracer
|
6
|
-
StackContext = Data.define(
|
7
|
-
:source,
|
8
|
-
:method_id,
|
9
|
-
:caller_location
|
10
|
-
)
|
11
|
-
|
12
6
|
# @return [Array<Symbol>]
|
13
7
|
def self.trace_events
|
14
8
|
@trace_events || %i[call c_call return c_return]
|
@@ -20,13 +14,13 @@ module DiverDown
|
|
20
14
|
end
|
21
15
|
|
22
16
|
# @param module_set [DiverDown::Trace::ModuleSet, Array<Module, String>]
|
23
|
-
# @param
|
17
|
+
# @param caller_paths [Array<String>, nil] if nil, trace all files
|
24
18
|
# @param ignored_method_ids [Array<String>]
|
25
19
|
# @param filter_method_id_path [#call, nil] filter method_id.path
|
26
20
|
# @param module_set [DiverDown::Trace::ModuleSet, nil] for optimization
|
27
|
-
def initialize(module_set: {},
|
28
|
-
if
|
29
|
-
raise ArgumentError, "
|
21
|
+
def initialize(module_set: {}, caller_paths: nil, ignored_method_ids: nil, filter_method_id_path: nil)
|
22
|
+
if caller_paths && !caller_paths.all? { Pathname.new(_1).absolute? }
|
23
|
+
raise ArgumentError, "caller_paths must be absolute path(#{caller_paths})"
|
30
24
|
end
|
31
25
|
|
32
26
|
@module_set = if module_set.is_a?(DiverDown::Trace::ModuleSet)
|
@@ -52,7 +46,7 @@ module DiverDown
|
|
52
46
|
DiverDown::Trace::IgnoredMethodIds.new(ignored_method_ids)
|
53
47
|
end
|
54
48
|
|
55
|
-
@
|
49
|
+
@caller_paths = caller_paths&.to_set
|
56
50
|
@filter_method_id_path = filter_method_id_path
|
57
51
|
end
|
58
52
|
|
@@ -80,10 +74,10 @@ module DiverDown
|
|
80
74
|
#
|
81
75
|
# @return [TracePoint]
|
82
76
|
def new_session(title: SecureRandom.uuid, definition_group: nil)
|
83
|
-
DiverDown::Trace::
|
77
|
+
DiverDown::Trace::Session.new(
|
84
78
|
module_set: @module_set,
|
85
79
|
ignored_method_ids: @ignored_method_ids,
|
86
|
-
|
80
|
+
caller_paths: @caller_paths,
|
87
81
|
filter_method_id_path: @filter_method_id_path,
|
88
82
|
definition: DiverDown::Definition.new(
|
89
83
|
title:,
|
data/lib/diver_down/trace.rb
CHANGED
@@ -5,7 +5,7 @@ require 'active_support/inflector'
|
|
5
5
|
module DiverDown
|
6
6
|
module Trace
|
7
7
|
require 'diver_down/trace/tracer'
|
8
|
-
require 'diver_down/trace/
|
8
|
+
require 'diver_down/trace/session'
|
9
9
|
require 'diver_down/trace/call_stack'
|
10
10
|
require 'diver_down/trace/module_set'
|
11
11
|
require 'diver_down/trace/redefine_ruby_methods'
|
data/lib/diver_down/version.rb
CHANGED
@@ -119,8 +119,9 @@ module DiverDown
|
|
119
119
|
# @param per [Integer]
|
120
120
|
# @param title [String]
|
121
121
|
# @param source [String]
|
122
|
-
|
123
|
-
|
122
|
+
# @param definition_group [String]
|
123
|
+
def definitions(page:, per:, title:, source:, definition_group:)
|
124
|
+
definition_enumerator = DiverDown::Web::DefinitionEnumerator.new(@store, title:, source:, definition_group:)
|
124
125
|
definitions, pagination = paginate(definition_enumerator, page, per)
|
125
126
|
|
126
127
|
json(
|
@@ -7,10 +7,12 @@ module DiverDown
|
|
7
7
|
|
8
8
|
# @param store [DiverDown::Definition::Store]
|
9
9
|
# @param query [String]
|
10
|
-
|
10
|
+
# @param definition_group [String]
|
11
|
+
def initialize(store, title: '', source: '', definition_group: '')
|
11
12
|
@store = store
|
12
13
|
@title = title
|
13
14
|
@source = source
|
15
|
+
@definition_group = definition_group
|
14
16
|
end
|
15
17
|
|
16
18
|
# @yield [parent_bit_id, bit_id, definition]
|
@@ -47,6 +49,10 @@ module DiverDown
|
|
47
49
|
matched &&= definition.sources.any? { _1.source_name.include?(@source) }
|
48
50
|
end
|
49
51
|
|
52
|
+
unless @definition_group.empty?
|
53
|
+
matched &&= definition.definition_group.to_s.include?(@definition_group)
|
54
|
+
end
|
55
|
+
|
50
56
|
matched
|
51
57
|
end
|
52
58
|
end
|
data/lib/diver_down/web.rb
CHANGED
@@ -45,7 +45,8 @@ module DiverDown
|
|
45
45
|
page: request.params['page']&.to_i || 1,
|
46
46
|
per: request.params['per']&.to_i || 100,
|
47
47
|
title: request.params['title'] || '',
|
48
|
-
source: request.params['source'] || ''
|
48
|
+
source: request.params['source'] || '',
|
49
|
+
definition_group: request.params['definition_group'] || ''
|
49
50
|
)
|
50
51
|
in ['GET', %r{\A/api/sources\.json\z}]
|
51
52
|
action.sources
|