rubylens 0.1.0.pre.1

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.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module RubyLens
6
+ class ReportAssetAssembler
7
+ STYLES_PLACEHOLDER = "{{REPORT_STYLES}}"
8
+ RUNTIME_PLACEHOLDER = "{{REPORT_RUNTIME}}"
9
+
10
+ def initialize(
11
+ shell_path: File.expand_path("../../assets/shells/report.html", __dir__),
12
+ stylesheet_path: File.expand_path("../../assets/styles/report.css", __dir__),
13
+ runtime_path: File.expand_path("../../assets/runtime/report.js", __dir__)
14
+ )
15
+ @shell_path = shell_path
16
+ @stylesheet_path = stylesheet_path
17
+ @runtime_path = runtime_path
18
+ end
19
+
20
+ def assemble
21
+ shell = File.read(@shell_path)
22
+ validate_placeholder(shell, STYLES_PLACEHOLDER)
23
+ validate_placeholder(shell, RUNTIME_PLACEHOLDER)
24
+
25
+ shell
26
+ .sub(STYLES_PLACEHOLDER, File.read(@stylesheet_path))
27
+ .sub(RUNTIME_PLACEHOLDER, File.read(@runtime_path))
28
+ end
29
+
30
+ private
31
+
32
+ def validate_placeholder(shell, placeholder)
33
+ return if shell.scan(placeholder).length == 1
34
+
35
+ raise Error, "report shell must contain exactly one #{placeholder} placeholder"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "fileutils"
5
+ require "json"
6
+ require "securerandom"
7
+ require_relative "report_asset_assembler"
8
+
9
+ module RubyLens
10
+ class ReportWriter
11
+ MODEL_PLACEHOLDER = "{{MODEL_BASE64}}"
12
+
13
+ def initialize(template_path: nil, asset_assembler: nil)
14
+ raise ArgumentError, "provide template_path or asset_assembler, not both" if template_path && asset_assembler
15
+
16
+ @template_path = template_path
17
+ @asset_assembler = asset_assembler || ReportAssetAssembler.new unless template_path
18
+ end
19
+
20
+ def write(model, output:)
21
+ output = File.expand_path(output)
22
+ directory = File.dirname(output)
23
+ FileUtils.mkdir_p(directory, mode: 0o700)
24
+ protect_default_directory(directory)
25
+ template = @template_path ? File.read(@template_path) : @asset_assembler.assemble
26
+ unless template.scan(MODEL_PLACEHOLDER).length == 1
27
+ raise Error, "report template must contain exactly one #{MODEL_PLACEHOLDER} placeholder"
28
+ end
29
+
30
+ payload = Base64.strict_encode64(JSON.generate(model))
31
+ html = template.sub(MODEL_PLACEHOLDER, payload)
32
+ atomic_write(output, html)
33
+ output
34
+ end
35
+
36
+ def rubylens_report?(path)
37
+ File.file?(path) && File.open(path, "rb") { |file| file.read(2048).include?('<meta name="generator" content="RubyLens">') }
38
+ rescue Errno::ENOENT, Errno::EACCES
39
+ false
40
+ end
41
+
42
+ private
43
+
44
+ def protect_default_directory(directory)
45
+ return unless File.basename(directory) == ".rubylens"
46
+
47
+ ignore = File.join(directory, ".gitignore")
48
+ atomic_write(ignore, "*\n") unless File.exist?(ignore)
49
+ end
50
+
51
+ def atomic_write(path, contents)
52
+ temporary = File.join(File.dirname(path), ".#{File.basename(path)}.#{SecureRandom.hex(6)}.tmp")
53
+ File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(contents) }
54
+ File.chmod(0o600, temporary)
55
+ File.rename(temporary, path)
56
+ File.chmod(0o600, path)
57
+ ensure
58
+ FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generator"
4
+ require_relative "showcase_model"
5
+ require_relative "showcase_writer"
6
+
7
+ module RubyLens
8
+ class ShowcaseGenerator
9
+ DEFAULT_SHOWCASE_NAME = "rubylens-showcase.html"
10
+
11
+ def initialize(
12
+ manifest_builder: Index::Manifest,
13
+ adapter: Index::RubydexAdapter.new,
14
+ model_builder: ArtModelBuilder.new,
15
+ showcase_model: ShowcaseModel.new,
16
+ showcase_writer: ShowcaseWriter.new,
17
+ pipeline: nil
18
+ )
19
+ @pipeline = pipeline || GenerationPipeline.new(manifest_builder:, adapter:, model_builder:)
20
+ @showcase_model = showcase_model
21
+ @showcase_writer = showcase_writer
22
+ end
23
+
24
+ def call(path: Dir.pwd, output: nil, lockfile: nil)
25
+ root = File.realpath(path)
26
+ if output.nil?
27
+ output = File.join(root, DEFAULT_SHOWCASE_NAME)
28
+ if File.exist?(output) && !@showcase_writer.rubylens_showcase?(output)
29
+ raise Error, "default showcase path already exists and is not a RubyLens showcase"
30
+ end
31
+ GitRepository.new(root).exclude_local(output, description: "showcase")
32
+ end
33
+ model, warnings = @pipeline.call(root:, lockfile:)
34
+ output_path = @showcase_writer.write(@showcase_model.call(model), output: output)
35
+
36
+ Result.new(output_path:, counts: model.fetch("totals").freeze, warnings:)
37
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => error
38
+ raise Error, error.message
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "art_model_builder"
4
+ require_relative "errors"
5
+
6
+ module RubyLens
7
+ class ShowcaseModel
8
+ SIGNAL_FIELDS = ArtModelBuilder::SIGNAL_FIELDS
9
+ TOTAL_FIELDS = %w[namespaces packages dependencyStars renderedDependencyStars].freeze
10
+ CATEGORY_FIELDS = %w[core tests].freeze
11
+
12
+ def call(model)
13
+ {
14
+ "schema" => "rubylens.showcase.v1",
15
+ "projectName" => model.fetch("projectName"),
16
+ "totals" => project_hash(model.fetch("totals"), TOTAL_FIELDS),
17
+ "domains" => project_hash(model.fetch("domains"), SIGNAL_FIELDS),
18
+ "categoryStats" => CATEGORY_FIELDS.to_h do |category|
19
+ [category, numeric_row(model.fetch("categoryStats").fetch(category), 4)]
20
+ end,
21
+ "namespaces" => model.fetch("namespaces").map { |row| numeric_row(row, 15) },
22
+ "packages" => model.fetch("packages").map { |row| numeric_row(row, 8) },
23
+ "dependencyStars" => model.fetch("dependencyStars").map { |row| numeric_row(row, 8) },
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ def project_hash(source, fields)
30
+ fields.to_h { |field| [field, Integer(source.fetch(field))] }
31
+ end
32
+
33
+ def numeric_row(row, length)
34
+ raise Error, "showcase model row has an unexpected shape" unless row.length >= length
35
+
36
+ row.first(length).map { |value| Integer(value) }
37
+ rescue ArgumentError, TypeError
38
+ raise Error, "showcase model rows must contain only numbers"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "report_writer"
4
+
5
+ module RubyLens
6
+ class ShowcaseWriter < ReportWriter
7
+ MARKER = '<meta name="rubylens-artifact" content="showcase">'
8
+ SHELL_PATH = File.expand_path("../../assets/shells/showcase.html", __dir__)
9
+ STYLESHEET_PATH = File.expand_path("../../assets/styles/showcase.css", __dir__)
10
+ RUNTIME_PATH = File.expand_path("../../assets/runtime/report.js", __dir__)
11
+
12
+ def initialize(template_path: nil, asset_assembler: nil)
13
+ asset_assembler ||= ReportAssetAssembler.new(
14
+ shell_path: SHELL_PATH,
15
+ stylesheet_path: STYLESHEET_PATH,
16
+ runtime_path: RUNTIME_PATH,
17
+ ) unless template_path
18
+ super(template_path:, asset_assembler:)
19
+ end
20
+
21
+ def rubylens_showcase?(path)
22
+ File.file?(path) && File.open(path, "rb") { |file| file.read(2048).include?(MARKER) }
23
+ rescue Errno::ENOENT, Errno::EACCES
24
+ false
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLens
4
+ VERSION = "0.1.0.pre.1"
5
+ end
data/lib/rubylens.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubylens/version"
4
+ require_relative "rubylens/errors"
5
+ require_relative "rubylens/paths"
6
+ require_relative "rubylens/git_repository"
7
+ require_relative "rubylens/index/manifest"
8
+ require_relative "rubylens/index/rubydex_adapter"
9
+ require_relative "rubylens/art_model_builder"
10
+ require_relative "rubylens/report_asset_assembler"
11
+ require_relative "rubylens/report_writer"
12
+ require_relative "rubylens/generator"
13
+ require_relative "rubylens/showcase_model"
14
+ require_relative "rubylens/showcase_writer"
15
+ require_relative "rubylens/showcase_generator"
16
+
17
+ module RubyLens
18
+ module_function
19
+
20
+ def generate_report(path: Dir.pwd, output: nil, lockfile: nil)
21
+ Generator.new.call(path: path, output: output, lockfile: lockfile)
22
+ end
23
+
24
+ def generate_showcase(path: Dir.pwd, output: nil, lockfile: nil)
25
+ ShowcaseGenerator.new.call(path: path, output: output, lockfile: lockfile)
26
+ end
27
+
28
+ def generate(path: Dir.pwd, output: nil, lockfile: nil)
29
+ generate_report(path: path, output: output, lockfile: lockfile)
30
+ end
31
+ end
32
+
33
+ require_relative "rubylens/cli"
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubylens
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - RubyLens contributors
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: rubydex
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.2.9
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.2.9
26
+ - !ruby/object:Gem::Dependency
27
+ name: base64
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.2'
40
+ description: |-
41
+ RubyLens uses Rubydex to index Ruby semantics and generate private, local-first
42
+ data for local interactive reports and autonomous visual showcases.
43
+ executables:
44
+ - rubylens
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.txt
49
+ - README.md
50
+ - assets/runtime/report.js
51
+ - assets/shells/report.html
52
+ - assets/shells/showcase.html
53
+ - assets/styles/report.css
54
+ - assets/styles/showcase.css
55
+ - exe/rubylens
56
+ - lib/rubylens.rb
57
+ - lib/rubylens/art_model_builder.rb
58
+ - lib/rubylens/cli.rb
59
+ - lib/rubylens/errors.rb
60
+ - lib/rubylens/generator.rb
61
+ - lib/rubylens/git_repository.rb
62
+ - lib/rubylens/index/manifest.rb
63
+ - lib/rubylens/index/rubydex_adapter.rb
64
+ - lib/rubylens/model/dependency_aggregation.rb
65
+ - lib/rubylens/paths.rb
66
+ - lib/rubylens/report_asset_assembler.rb
67
+ - lib/rubylens/report_writer.rb
68
+ - lib/rubylens/showcase_generator.rb
69
+ - lib/rubylens/showcase_model.rb
70
+ - lib/rubylens/showcase_writer.rb
71
+ - lib/rubylens/version.rb
72
+ homepage: https://st0012.dev/rails-galaxy/
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ allowed_push_host: https://rubygems.org
77
+ homepage_uri: https://st0012.dev/rails-galaxy/
78
+ rubygems_mfa_required: 'true'
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.2.0
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: 4.1.dev
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 4.0.10
97
+ specification_version: 4
98
+ summary: Turn a Ruby codebase into self-contained stellar HTML
99
+ test_files: []