captive-stack-detector 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '086052c42d95d9caf63c44c467e86e28c24c940c260d7752616ce8da6cea846f'
4
+ data.tar.gz: ab4f55fb35c1476f42e166a8df0f0f68ae7f13ea8b920061bd603c6f0cbb3d24
5
+ SHA512:
6
+ metadata.gz: 001df9a9e62af902bc8bbd8816991a5c9af415d0a815912217c4ef0fe8dda479f4228cde5be4fa8a715eaf22fd109e335f73db182597fc0d8b737c9b53f866e9
7
+ data.tar.gz: c986ed711a7475e9c9f585e050f3d9deb7acc46e5f86dba6ffdcfa0ba7d03f0af5c7057c1e4d7ec937083f6f4276e62d8a2d8b66bbef70dacb6af03ef0c4c51b
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../captive_stack_detector"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ module EnvVarsScanner
5
+ HANDLED_VARS = %w[
6
+ RAILS_ENV SECRET_KEY_BASE DATABASE_URL REDIS_URL
7
+ RAILS_LOG_TO_STDOUT RAILS_SERVE_STATIC_FILES PORT RACK_ENV
8
+ ].freeze
9
+
10
+ ENV_KEY_PATTERN = /ENV\.fetch\(['"]([A-Z_][A-Z0-9_]*)['"]|ENV\[['"]([A-Z_][A-Z0-9_]*)['"]\]/
11
+ SAFE_DEFAULT_PATTERN = /\A,\s*(?:nil\b|true\b|false\b|'[^']*'|"[^"]*"|\d+)\s*[,)]/
12
+ BLOCK_DEFAULT_PATTERN = /\A\s*\)\s*(?:do|\{)/
13
+ ASSIGNMENT_PATTERN = /\A\s*\|\|=/
14
+
15
+ def self.scan(content)
16
+ result = {}
17
+ uncommented(content).scan(ENV_KEY_PATTERN) do |fetch_key, bracket_key|
18
+ after = Regexp.last_match.post_match
19
+ if bracket_key
20
+ next if ASSIGNMENT_PATTERN.match?(after)
21
+
22
+ result[bracket_key] ||= "placeholder"
23
+ else
24
+ next if SAFE_DEFAULT_PATTERN.match?(after) || BLOCK_DEFAULT_PATTERN.match?(after)
25
+
26
+ result[fetch_key] ||= "placeholder"
27
+ end
28
+ end
29
+ result.reject { |k, _| HANDLED_VARS.include?(k) }
30
+ end
31
+
32
+ def self.uncommented(content)
33
+ content.lines.reject { |l| l.match?(/^\s*#/) }.join
34
+ end
35
+ private_class_method :uncommented
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "env_vars_scanner"
4
+ require_relative "node_version_detector"
5
+ require_relative "ruby_version_detector"
6
+
7
+ module CaptiveStackDetector
8
+ class FileContentParser
9
+ def initialize(reader)
10
+ @reader = reader
11
+ end
12
+
13
+ def ruby_version
14
+ RubyVersionDetector.new(@reader).detect
15
+ end
16
+
17
+ def node_version
18
+ NodeVersionDetector.new(@reader).detect
19
+ end
20
+
21
+ def env_vars
22
+ content = @reader.read("config/storage.yml")
23
+ content ? EnvVarsScanner.scan(content) : {}
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require_relative "file_content_parser"
5
+ require_relative "github_api_client"
6
+
7
+ module CaptiveStackDetector
8
+ class FileReader
9
+ def self.build(local_path: nil, github_token: nil, repo: nil)
10
+ return LocalFileReader.new(local_path) if local_path
11
+ return GithubFileReader.new(github_token, repo) if github_token && repo
12
+
13
+ raise ArgumentError, "local_path: ou github_token: + repo: requis"
14
+ end
15
+ end
16
+
17
+ class LocalFileReader
18
+ extend Forwardable
19
+
20
+ def_delegators :@parser, :ruby_version, :node_version, :env_vars
21
+
22
+ def initialize(path)
23
+ @path = path
24
+ @parser = FileContentParser.new(self)
25
+ end
26
+
27
+ def read(filename)
28
+ full = File.join(@path, filename)
29
+ File.read(full, encoding: "utf-8") if File.exist?(full)
30
+ end
31
+ end
32
+
33
+ class GithubFileReader
34
+ extend Forwardable
35
+
36
+ def_delegators :@parser, :ruby_version, :node_version, :env_vars
37
+
38
+ def initialize(token, repo)
39
+ @client = GithubApiClient.new(token, repo)
40
+ @parser = FileContentParser.new(self)
41
+ end
42
+
43
+ def read(filename)
44
+ @client.fetch(filename)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ class GemfileAnalyzer
5
+ ASSET_GEMS = %w[
6
+ sprockets sprockets-rails propshaft importmap-rails
7
+ cssbundling-rails jsbundling-rails dartsass-rails tailwindcss-rails
8
+ ].freeze
9
+
10
+ REDIS_GEMS = %w[redis redis-client sidekiq].freeze
11
+
12
+ def initialize(gemfile)
13
+ @gemfile = gemfile
14
+ end
15
+
16
+ def rails? = gem?("rails")
17
+ def subtype = ASSET_GEMS.any? { |g| gem?(g) } ? "app" : "api"
18
+ def database = gem?("pg") ? "postgres" : nil
19
+ def queue = REDIS_GEMS.any? { |g| gem?(g) } ? "redis" : nil
20
+
21
+ def worker_command(procfile)
22
+ if procfile
23
+ procfile.each_line do |line|
24
+ m = line.match(/^worker:\s*(.+)$/)
25
+ return m[1].strip if m
26
+ end
27
+ end
28
+ gem?("sidekiq") ? "bundle exec sidekiq" : nil
29
+ end
30
+
31
+ private
32
+
33
+ def gem?(name)
34
+ @gemfile.match?(/gem ['"]#{Regexp.escape(name)}['"]/)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "base64"
6
+ require "json"
7
+
8
+ module CaptiveStackDetector
9
+ class GithubApiClient
10
+ def initialize(token, repo)
11
+ @token = token
12
+ @repo = repo
13
+ end
14
+
15
+ def fetch(filename)
16
+ uri = URI("https://api.github.com/repos/#{@repo}/contents/#{filename}")
17
+ req = Net::HTTP::Get.new(uri)
18
+ req["Authorization"] = "Bearer #{@token}"
19
+ req["Accept"] = "application/vnd.github+json"
20
+
21
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
22
+ return nil unless res.is_a?(Net::HTTPSuccess)
23
+
24
+ Base64.decode64(JSON.parse(res.body)["content"]).force_encoding("utf-8")
25
+ rescue StandardError
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../captive_stack_detector"
4
+ require_relative "package_json_analyzer"
5
+
6
+ module CaptiveStackDetector
7
+ class JsStackDetector
8
+ def initialize(reader, package_json)
9
+ @reader = reader
10
+ @analyzer = PackageJsonAnalyzer.new(package_json)
11
+ end
12
+
13
+ def detect
14
+ type = @analyzer.type
15
+ raise UnsupportedStack unless type
16
+
17
+ Result.new(
18
+ type: type,
19
+ subtype: nil,
20
+ services: Services.new(database: @analyzer.database, queue: @analyzer.queue),
21
+ worker: build_worker,
22
+ runtime: Runtime.new(ruby: nil, node: @reader.node_version),
23
+ env_vars: {},
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def build_worker
30
+ procfile = @reader.read("Procfile")
31
+ return nil unless procfile
32
+
33
+ procfile.each_line do |line|
34
+ m = line.match(/^worker:\s*(.+)$/)
35
+ return Worker.new(command: m[1].strip) if m
36
+ end
37
+ nil
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module CaptiveStackDetector
6
+ class NodeVersionDetector
7
+ def initialize(reader)
8
+ @reader = reader
9
+ end
10
+
11
+ def detect
12
+ return from_nvmrc if (nvmrc = @reader.read(".nvmrc"))
13
+ return from_tool_versions if (tv = @reader.read(".tool-versions"))
14
+ return from_package_json if (pkg = @reader.read("package.json"))
15
+
16
+ nil
17
+ end
18
+
19
+ private
20
+
21
+ def from_nvmrc
22
+ @reader.read(".nvmrc").strip.sub(/^v/, "").split(".").first
23
+ end
24
+
25
+ def from_tool_versions
26
+ m = @reader.read(".tool-versions")&.match(/^nodejs\s+(\d+)/)
27
+ m&.[](1)
28
+ end
29
+
30
+ def from_package_json
31
+ m = JSON.parse(@reader.read("package.json")).dig("engines", "node")&.match(/\d+/)
32
+ m&.[](0)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module CaptiveStackDetector
6
+ class PackageJsonAnalyzer
7
+ def initialize(package_json)
8
+ @parsed = JSON.parse(package_json)
9
+ end
10
+
11
+ def type
12
+ return "expo" if deps.key?("expo")
13
+ return "node" if @parsed.dig("scripts", "start")
14
+
15
+ nil
16
+ end
17
+
18
+ def database
19
+ deps.key?("pg") ? "postgres" : nil
20
+ end
21
+
22
+ def queue
23
+ (deps.key?("redis") || deps.key?("ioredis")) ? "redis" : nil
24
+ end
25
+
26
+ private
27
+
28
+ def deps
29
+ @parsed.fetch("dependencies", {}).merge(@parsed.fetch("devDependencies", {}))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../captive_stack_detector"
4
+
5
+ module CaptiveStackDetector
6
+ class RailsStackDetector
7
+ def initialize(reader, analyzer)
8
+ @reader = reader
9
+ @analyzer = analyzer
10
+ end
11
+
12
+ def detect
13
+ raise UnsupportedStack unless @analyzer.rails?
14
+
15
+ Result.new(
16
+ type: "rails",
17
+ subtype: @analyzer.subtype,
18
+ services: Services.new(database: @analyzer.database, queue: @analyzer.queue),
19
+ worker: build_worker,
20
+ runtime: Runtime.new(ruby: @reader.ruby_version, node: nil),
21
+ env_vars: @reader.env_vars,
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def build_worker
28
+ command = @analyzer.worker_command(@reader.read("Procfile"))
29
+ command ? Worker.new(command: command) : nil
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ class RubyVersionDetector
5
+ def initialize(reader)
6
+ @reader = reader
7
+ end
8
+
9
+ def detect
10
+ return from_tool_versions if @reader.read(".tool-versions")
11
+ return from_ruby_version if @reader.read(".ruby-version")
12
+ return from_gemfile if @reader.read("Gemfile")
13
+
14
+ nil
15
+ end
16
+
17
+ private
18
+
19
+ def from_tool_versions
20
+ @reader.read(".tool-versions")&.match(/^ruby\s+(\S+)/)&.[](1)
21
+ end
22
+
23
+ def from_ruby_version
24
+ @reader.read(".ruby-version")&.strip&.sub(/^ruby-/, "")
25
+ end
26
+
27
+ def from_gemfile
28
+ @reader.read("Gemfile")&.match(/^\s*ruby\s+['"](\d+\.\d+\.\d+)['"]/)&.[](1)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ Services = Data.define(:database, :queue)
5
+ Worker = Data.define(:command)
6
+ Runtime = Data.define(:ruby, :node)
7
+ Result = Data.define(:type, :subtype, :services, :worker, :runtime, :env_vars)
8
+
9
+ UnsupportedStack = Class.new(StandardError)
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "captive_stack_detector/types"
5
+ require_relative "captive_stack_detector/file_reader"
6
+ require_relative "captive_stack_detector/gemfile_analyzer"
7
+ require_relative "captive_stack_detector/rails_stack_detector"
8
+ require_relative "captive_stack_detector/js_stack_detector"
9
+
10
+ module CaptiveStackDetector
11
+ def self.detect(local_path: nil, github_token: nil, repo: nil)
12
+ reader = FileReader.build(local_path: local_path, github_token: github_token, repo: repo)
13
+ detect_from(reader)
14
+ end
15
+
16
+ def self.detect_from(reader)
17
+ gemfile = reader.read("Gemfile")
18
+ package_json = reader.read("package.json")
19
+
20
+ return RailsStackDetector.new(reader, GemfileAnalyzer.new(gemfile)).detect if gemfile
21
+ return JsStackDetector.new(reader, package_json).detect if package_json
22
+
23
+ raise UnsupportedStack
24
+ end
25
+ private_class_method :detect_from
26
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: captive-stack-detector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Captive Studio
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Logique pure sans I/O : reçoit des strings de contenu de fichiers, retourne
13
+ un StackResult typé.'
14
+ email:
15
+ - dev@captive.fr
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/captive/stack/detector.rb
21
+ - lib/captive_stack_detector.rb
22
+ - lib/captive_stack_detector/env_vars_scanner.rb
23
+ - lib/captive_stack_detector/file_content_parser.rb
24
+ - lib/captive_stack_detector/file_reader.rb
25
+ - lib/captive_stack_detector/gemfile_analyzer.rb
26
+ - lib/captive_stack_detector/github_api_client.rb
27
+ - lib/captive_stack_detector/js_stack_detector.rb
28
+ - lib/captive_stack_detector/node_version_detector.rb
29
+ - lib/captive_stack_detector/package_json_analyzer.rb
30
+ - lib/captive_stack_detector/rails_stack_detector.rb
31
+ - lib/captive_stack_detector/ruby_version_detector.rb
32
+ - lib/captive_stack_detector/types.rb
33
+ - lib/captive_stack_detector/version.rb
34
+ homepage: https://github.com/captive-studio/captive-stack-detector
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '3.2'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 4.0.10
53
+ specification_version: 4
54
+ summary: Détection de stack (Rails, Node, Expo) à partir du contenu de fichiers de
55
+ repo
56
+ test_files: []