ficha 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ac8590b6ab46f565f13b1bb959e55c790f8ecebb62a7946b47be7e3acd94a88
4
+ data.tar.gz: 6290916d3e12018298de9343a6d4803ce2b7beb5d983c19a9f26c1b32993426f
5
+ SHA512:
6
+ metadata.gz: 68dbe337310fa333176a6b7341d479dec441af04de24dc1e811307631432c3d459b4aaf31fbbf5272148e2b9cef6971aeb9f43e6703f129b2c9314d66236a4d7
7
+ data.tar.gz: b5216fd9da5ed299891069e10101af2598a3a68c36cdd31ef2b02b7d59e42be5a1d32dd596d8aaf963418c57e752718d44fa5167371362aa15b8c9f7a1c8fe07
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to `ficha` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Changed
11
+
12
+ - Refactored `Ficha::CLI#run` into smaller private methods to improve readability and comply with RuboCop metrics (ABC size, method length).
13
+
14
+ ### Fixed
15
+
16
+ - Auto-corrected code style with RuboCop.
17
+
18
+ ### Internal
19
+
20
+ - Added RuboCop linting to GitHub Actions CI.
21
+
22
+ ## [0.1.0] - 2025-12-09
23
+
24
+ ### Added
25
+
26
+ - Initial CLI with `--help` flag and basic argument parsing.
27
+ - Core `Engine` class for resolving and filtering project files based on strategies.
28
+ - Support for `--dry-run` mode with end-to-end tests.
29
+ - Config file loading mechanism.
30
+ - Basic templating capabilities (foundation for future expansion).
31
+ - End-to-end test suite with fixtures.
32
+
33
+ ### Internal
34
+
35
+ - Improved test fixtures and added e2e test scaffolding.
36
+ - Finalized `ficha.gemspec` for public release under AGPL-3.0.
37
+ - Added LICENSE file (AGPL-3.0).
38
+ - Initial `.gitignore` and gem scaffolding.
39
+
40
+ ### Changelog
41
+
42
+ - This changelog was introduced retroactively to document project evolution from the first commit.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # ficha
2
+
3
+ > **Feature-aware file dumper for Ruby projects**
4
+ > Extract only the files you need — by model, endpoint, feature, or safe subset — without manual grepping.
5
+
6
+ ![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)
7
+ ![Ruby](https://img.shields.io/badge/ruby-3.0%2B-red)
8
+
9
+ `ficha` helps you **dump relevant files** from large Ruby/Rails codebases based on **semantic strategies**, not just file extensions.
10
+ Think of it as `grep` + `find` + domain knowledge — for developers who know *what* they need, but not *where* it is.
11
+
12
+ Perfect for:
13
+
14
+ - Preparing minimal bug reproductions
15
+ - Sharing feature context with teammates
16
+ - Migrating subsystems
17
+ - Auditing legacy code
18
+ - Generating focused backups
19
+
20
+ ---
21
+
22
+ ## 🔍 Quick Example
23
+
24
+ ```sh
25
+ # Dump all files related to the `User` model
26
+ ficha by-model:User
27
+
28
+ # Dump a "safe" subset (models, serializers, controllers, specs)
29
+ ficha full-safe
30
+
31
+ # Preview without writing
32
+ ficha by-model:Order --dry-run
33
+ ```
34
+
35
+ Output (dry-run):
36
+
37
+ ```
38
+ app/models/user.rb
39
+ app/controllers/users_controller.rb
40
+ app/serializers/user_serializer.rb
41
+ spec/models/user_spec.rb
42
+ ...
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 🧩 Core Concepts
48
+
49
+ `ficha` works by combining:
50
+
51
+ 1. **Strategies** — rules that define *what to include* (e.g. `by-model`, `full-safe`)
52
+ 2. **Config** — your project-specific paths, conventions, and overrides (`ficha.yml`)
53
+ 3. **Engine** — the resolver that applies strategies to your codebase
54
+ 4. **CLI** — your interface to run queries
55
+
56
+ ```mermaid
57
+ graph LR
58
+ A[CLI: ficha by-model:User] --> B(Engine)
59
+ B --> C{Strategy: by-model}
60
+ B --> D[Config: ficha.yml]
61
+ C --> E[Match model, controller, serializer...]
62
+ D --> F[Custom paths, excludes]
63
+ E --> G[File list]
64
+ F --> G
65
+ G --> H[Output or copy]
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🚀 Installation
71
+
72
+ Add to your project’s `Gemfile`:
73
+
74
+ ```ruby
75
+ group :development do
76
+ gem "ficha", require: false
77
+ end
78
+ ```
79
+
80
+ Then run:
81
+
82
+ ```sh
83
+ bundle install
84
+ ```
85
+
86
+ > 💡 `ficha` is designed as a **dev-only CLI tool** — it doesn’t affect your runtime.
87
+
88
+ ---
89
+
90
+ ## ⚙️ Configuration
91
+
92
+ By default, `ficha` looks for `config/ficha.yml` (in Rails) or `.ficha.yml` (in any project).
93
+
94
+ ### Example `config/ficha.yml`
95
+
96
+ ```yaml
97
+ # ficha.yml — declarative, extensible, no Ruby code!
98
+
99
+ strategies:
100
+ by-model:
101
+ include:
102
+ - "app/models/{{name}}.rb"
103
+ - "app/controllers/{{name.pluralize}}_controller.rb"
104
+ - "app/serializers/{{name}}_serializer.rb"
105
+ - "spec/models/{{name}}_spec.rb"
106
+ - "spec/requests/{{name.pluralize}}_spec.rb"
107
+ exclude:
108
+ - "app/models/concerns/*"
109
+
110
+ full-safe:
111
+ include:
112
+ - "app/models/**/*.rb"
113
+ - "app/controllers/**/*.rb"
114
+ - "app/serializers/**/*.rb"
115
+ - "spec/{models,controllers,serializers}/**/*.rb"
116
+ exclude:
117
+ - "spec/support/**/*"
118
+
119
+ paths:
120
+ root: "." # project root
121
+ output: "/tmp/ficha-out" # (future: where to copy files)
122
+
123
+ excludes:
124
+ - "tmp/**/*"
125
+ - "log/**/*"
126
+ - "vendor/**/*"
127
+ ```
128
+
129
+ > ✅ **Philosophy**: Keep logic in config, not code.
130
+ > 🔒 Config supports ERB if you *really* need dynamic values (but avoid it for reproducibility).
131
+
132
+ ---
133
+
134
+ ## 🎯 Built-in Strategies
135
+
136
+ | Strategy | Purpose |
137
+ |---------------|--------|
138
+ | `full-safe` | Dumps core app code: models, controllers, serializers, related specs |
139
+ | `by-model:X` | Dumps everything related to model `X` (convention-based) |
140
+ | *(more coming)* | — |
141
+
142
+ You can **extend strategies** in your `ficha.yml` — no need to fork the gem.
143
+
144
+ ---
145
+
146
+ ## 🛠 CLI Usage
147
+
148
+ ```text
149
+ ficha [STRATEGY:PARAMS...] [options]
150
+
151
+ Examples:
152
+ ficha # runs 'full-safe'
153
+ ficha by-model:User # model-aware dump
154
+ ficha full-safe --dry-run # preview only
155
+
156
+ Options:
157
+ --help Show help
158
+ --dry-run Print matched files (default)
159
+ --exclude GLOB Exclude additional paths (can be used multiple times)
160
+ ```
161
+
162
+ > ⚠️ **Non-dry-run mode** (actual file copying) is **not implemented yet** — coming soon!
163
+ > You’ll be able to dump to a directory, zip, or tar.
164
+
165
+ ---
166
+
167
+ ## 🧪 Testing & Development
168
+
169
+ ```sh
170
+ bundle exec rake # runs tests + RuboCop
171
+ bundle exec bin/ficha # run from source
172
+ ```
173
+
174
+ Tests use real fixtures in `test/fixtures/rails_app/` — easy to extend.
175
+
176
+ ---
177
+
178
+ ## 📜 License
179
+
180
+ AGPL-3.0 — because **privacy, self-hosting, and user freedom matter**.
181
+
182
+ You’re free to use `ficha` in any project, but if you modify and redistribute it (e.g. as a hosted service), you must share the source.
183
+
184
+ ---
185
+
186
+ ## 🙌 Contributing
187
+
188
+ PRs welcome! Especially:
189
+
190
+ - New strategies
191
+ - Non-dry-run output formats (dir, zip, tar)
192
+ - Better Rails/7+ support
193
+ - Performance optimizations
194
+
195
+ Please:
196
+
197
+ - Write tests
198
+ - Keep config declarative
199
+ - Respect the AGPL spirit
200
+
201
+ ---
202
+
203
+ > Made with 💎 by a Ruby dev who hates manual file hunting.
204
+ > Inspired by `rails:template`, `bundle gem`, and the pain of legacy monoliths.
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ficha"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/ficha ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # bin/ficha
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ficha/cli"
8
+
9
+ Ficha::CLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
data/exe/ficha ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # exe/ficha
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+ require "ficha"
8
+
9
+ Ficha::CLI.start(ARGV)
data/ficha.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ficha/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ficha"
7
+ spec.version = Ficha::VERSION
8
+ spec.authors = ["Ilmir Karimov"]
9
+ spec.email = ["code.for.func@gmail.com"]
10
+
11
+ spec.summary = "Feature-aware file dumper for Ruby projects"
12
+ spec.description = <<~DESCRIPTION
13
+ Extract only the files you need — by model, endpoint, feature, or safe subset —
14
+ without manual grepping.
15
+ DESCRIPTION
16
+ spec.homepage = "https://github.com/ilmir/ficha"
17
+ spec.license = "AGPL-3.0-or-later"
18
+ spec.required_ruby_version = ">= 3.0"
19
+
20
+ spec.files = Dir[
21
+ "lib/**/*",
22
+ "bin/*",
23
+ "README.md",
24
+ "LICENSE.txt",
25
+ "CHANGELOG.md",
26
+ "ficha.gemspec"
27
+ ].reject { |f| File.directory?(f) }
28
+
29
+ spec.bindir = "exe"
30
+ spec.executables = ["ficha"]
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "activesupport", ">= 6.0", "< 9.0"
34
+
35
+ spec.metadata = {
36
+ "homepage_uri" => spec.homepage,
37
+ "source_code_uri" => "https://github.com/ilmir/ficha",
38
+ "changelog_uri" => "https://github.com/ilmir/ficha/blob/main/CHANGELOG.md",
39
+ "bug_tracker_uri" => "https://github.com/ilmir/ficha/issues",
40
+ "rubygems_mfa_required" => "true"
41
+ }
42
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Ficha
6
+ class Config
7
+ DEFAULT_CONFIG_PATH = File.expand_path("../../data/base.ficha.yml", __dir__)
8
+
9
+ def self.load(config_path = nil)
10
+ config_path ||= find_config
11
+ if config_path && File.exist?(config_path)
12
+ raw = YAML.safe_load_file(config_path, aliases: true)
13
+ raw ||= {}
14
+ else
15
+ raw = YAML.safe_load_file(DEFAULT_CONFIG_PATH, aliases: true)
16
+ end
17
+ new(raw)
18
+ end
19
+
20
+ def initialize(data)
21
+ @data = data
22
+ end
23
+
24
+ def strategies
25
+ @data.fetch("strategies", {})
26
+ end
27
+
28
+ def defaults
29
+ @data.fetch("defaults", {})
30
+ end
31
+
32
+ def self.find_config
33
+ ["./.ficha.yml", "./ficha.yml"].find { |f| File.exist?(f) }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "template"
5
+ require_relative "config"
6
+
7
+ module Ficha
8
+ class Engine
9
+ SENSITIVE_PATTERNS = [
10
+ /\.env/,
11
+ /master\.key/,
12
+ /credentials\.yml\.enc/
13
+ ].freeze
14
+
15
+ def initialize(config:, queries:, overrides: {})
16
+ @config = config
17
+ @queries = queries
18
+ @overrides = overrides
19
+ end
20
+
21
+ def run
22
+ files = @queries.flat_map { |q| resolve_query(q) }
23
+ files = apply_filters(files)
24
+ files.uniq.sort
25
+ end
26
+
27
+ private
28
+
29
+ def resolve_query(query_str)
30
+ parsed = parse_query(query_str)
31
+ strategy_name = parsed[:name]
32
+ params = parsed[:params]
33
+
34
+ strategy = @config.strategies[strategy_name]
35
+ raise "strategy '#{strategy_name}' not found in .ficha.yml" unless strategy
36
+
37
+ param_hash = build_param_hash(strategy["params"] || [], params)
38
+
39
+ # Раскрываем include_paths через шаблоны и glob
40
+ strategy["include_paths"].flat_map do |pattern|
41
+ rendered = Template.render(pattern, param_hash)
42
+ Dir.glob(rendered)
43
+ end
44
+ end
45
+
46
+ def parse_query(query_str)
47
+ if query_str.include?(":")
48
+ parts = query_str.split(":", 2)
49
+ name = parts[0]
50
+ param_str = parts[1]
51
+ params = param_str.include?(",") ? param_str.split(",") : [param_str]
52
+ { name: name, params: params }
53
+ else
54
+ { name: query_str, params: [] }
55
+ end
56
+ end
57
+
58
+ def build_param_hash(expected_params, given_params)
59
+ expected_params.map(&:to_sym).zip(given_params).to_h
60
+ end
61
+
62
+ def apply_filters(files)
63
+ files = filter_by_sensitive_names(files)
64
+ files = filter_by_cli_excludes(files)
65
+ filter_by_ignored_extensions(files)
66
+ end
67
+
68
+ def filter_by_sensitive_names(files)
69
+ files.reject do |path|
70
+ SENSITIVE_PATTERNS.any? { |pat| path.match?(pat) }
71
+ end
72
+ end
73
+
74
+ def filter_by_cli_excludes(files)
75
+ excludes = @overrides[:exclude_paths] || []
76
+ files.reject do |path|
77
+ excludes.any? do |exclude_glob|
78
+ # Если glob не содержит *, **, ? — считаем, что это директория → добавляем /**
79
+ glob = exclude_glob.include?("*") || exclude_glob.include?("?") ? exclude_glob : "#{exclude_glob}/**/*"
80
+ File.fnmatch(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
81
+ end
82
+ end
83
+ end
84
+
85
+ def filter_by_ignored_extensions(files)
86
+ ignored_exts = (@config.defaults["ignored_extensions"] || []).map(&:downcase)
87
+ files.reject do |path|
88
+ ext = File.extname(path).downcase
89
+ ignored_exts.include?(ext)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Ficha
6
+ module Template
7
+ FILTERS = {
8
+ "underscore" => lambda(&:underscore),
9
+ "pluralize" => lambda(&:pluralize),
10
+ "singularize" => lambda(&:singularize),
11
+ "camelize" => lambda(&:camelize)
12
+ }.freeze
13
+
14
+ def self.render(template_str, bindings = {})
15
+ template_str.gsub(/\{\{\s*(.+?)\s*\}\}/) do
16
+ expr = ::Regexp.last_match(1)
17
+ apply_filters(expr.strip, bindings)
18
+ end
19
+ end
20
+
21
+ def self.apply_filters(expr, bindings)
22
+ parts = expr.split("|").map(&:strip)
23
+ var_name = parts.first
24
+ value = bindings[var_name.to_sym] || var_name
25
+ apply_pipeline(value, parts.drop(1))
26
+ end
27
+
28
+ def self.apply_pipeline(value, filters)
29
+ filters.reduce(value) do |acc, filter_name|
30
+ FILTERS.fetch(filter_name, ->(x) { x }).call(acc)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ficha
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ficha.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ficha/version"
4
+ require_relative "ficha/config"
5
+ require_relative "ficha/template"
6
+ require_relative "ficha/engine"
7
+ require "optparse"
8
+
9
+ module Ficha
10
+ class CLI
11
+ def self.start(argv)
12
+ new(argv).run
13
+ end
14
+
15
+ def initialize(argv)
16
+ @argv = argv
17
+ @options = { dry_run: false }
18
+ @queries = []
19
+ end
20
+
21
+ def run
22
+ parse!
23
+
24
+ handle_help! if @options[:help]
25
+ ensure_default_query!
26
+
27
+ files = execute_engine
28
+ process_files(files)
29
+ rescue StandardError => e
30
+ warn "Error: #{e.message}"
31
+ exit 1
32
+ end
33
+
34
+ private
35
+
36
+ def handle_help!
37
+ puts help_text
38
+ exit 0
39
+ end
40
+
41
+ def ensure_default_query!
42
+ @queries << "full-safe" if @queries.empty?
43
+ end
44
+
45
+ def process_files(files)
46
+ if @options[:dry_run]
47
+ files.each { |f| puts f }
48
+ else
49
+ warn "Non-dry-run mode not implemented yet"
50
+ exit 1
51
+ end
52
+ end
53
+
54
+ def execute_engine
55
+ config = Config.load
56
+ engine = Engine.new(
57
+ config: config,
58
+ queries: @queries,
59
+ overrides: {
60
+ exclude_paths: @options[:exclude_paths],
61
+ dry_run: @options[:dry_run]
62
+ }
63
+ )
64
+ engine.run
65
+ end
66
+
67
+ def parse!
68
+ @options[:exclude_paths] = []
69
+
70
+ OptionParser.new do |opts|
71
+ opts.banner = "Usage: ficha [STRATEGY:PARAMS...] [options]"
72
+ opts.on("--help", "Show this help") { @options[:help] = true }
73
+ opts.on("--dry-run", "Show files without copying") { @options[:dry_run] = true }
74
+ opts.on("--exclude PATTERN", "Exclude paths matching glob pattern") do |pattern|
75
+ @options[:exclude_paths] << pattern
76
+ end
77
+ end.parse!(@argv)
78
+
79
+ @queries = @argv.dup
80
+ end
81
+
82
+ def help_text
83
+ <<~HELP
84
+ ficha — feature-aware file dumper for Ruby projects
85
+
86
+ Usage:
87
+ ficha [STRATEGY:PARAMS...] [options]
88
+
89
+ Examples:
90
+ ficha # full-safe dump
91
+ ficha by-model:User # dump User-related files
92
+ ficha full-safe --dry-run
93
+
94
+ Options:
95
+ HELP
96
+ end
97
+ end
98
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ficha
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ilmir Karimov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '6.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ description: |
33
+ Extract only the files you need — by model, endpoint, feature, or safe subset —
34
+ without manual grepping.
35
+ email:
36
+ - code.for.func@gmail.com
37
+ executables:
38
+ - ficha
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - CHANGELOG.md
43
+ - README.md
44
+ - bin/console
45
+ - bin/ficha
46
+ - bin/setup
47
+ - exe/ficha
48
+ - ficha.gemspec
49
+ - lib/ficha.rb
50
+ - lib/ficha/config.rb
51
+ - lib/ficha/engine.rb
52
+ - lib/ficha/template.rb
53
+ - lib/ficha/version.rb
54
+ homepage: https://github.com/ilmir/ficha
55
+ licenses:
56
+ - AGPL-3.0-or-later
57
+ metadata:
58
+ homepage_uri: https://github.com/ilmir/ficha
59
+ source_code_uri: https://github.com/ilmir/ficha
60
+ changelog_uri: https://github.com/ilmir/ficha/blob/main/CHANGELOG.md
61
+ bug_tracker_uri: https://github.com/ilmir/ficha/issues
62
+ rubygems_mfa_required: 'true'
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.6.9
78
+ specification_version: 4
79
+ summary: Feature-aware file dumper for Ruby projects
80
+ test_files: []