Skip to content

Commit

Permalink
Initial commit (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Evanczuk authored Dec 21, 2022
1 parent 8fe63d7 commit b4983b1
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 5 deletions.
118 changes: 118 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# The behavior of RuboCop can be controlled via the .rubocop.yml
# configuration file. It makes it possible to enable/disable
# certain cops (checks) and to alter their behavior if they accept
# any parameters. The file can be placed either in your home
# directory or in some project directory.
#
# RuboCop will start looking for the configuration file in the directory
# where the inspected file is and continue its way up to the root directory.
#
# See https://docs.rubocop.org/rubocop/configuration
AllCops:
SuggestExtensions: false
NewCops: enable
Exclude:
- vendor/bundle/**/**

Metrics/ParameterLists:
Enabled: false

# This cop is annoying with typed configuration
Style/TrivialAccessors:
Enabled: false

# This rubocop is annoying when we use interfaces a lot
Lint/UnusedMethodArgument:
Enabled: false

Gemspec/RequireMFA:
Enabled: false

Lint/DuplicateBranch:
Enabled: false

# If is sometimes easier to think about than unless sometimes
Style/NegatedIf:
Enabled: false

# Disabling for now until it's clearer why we want this
Style/FrozenStringLiteralComment:
Enabled: false

# It's nice to be able to read the condition first before reading the code within the condition
Style/GuardClause:
Enabled: false

#
# Leaving length metrics to human judgment for now
#
Metrics/ModuleLength:
Enabled: false

Layout/LineLength:
Enabled: false

Metrics/BlockLength:
Enabled: false

Metrics/MethodLength:
Enabled: false

Metrics/AbcSize:
Enabled: false

Metrics/ClassLength:
Enabled: false

# This doesn't feel useful
Metrics/CyclomaticComplexity:
Enabled: false

# This doesn't feel useful
Metrics/PerceivedComplexity:
Enabled: false

# It's nice to be able to read the condition first before reading the code within the condition
Style/IfUnlessModifier:
Enabled: false

# This leads to code that is not very readable at times (very long lines)
Style/ConditionalAssignment:
Enabled: false

# For now, we prefer to lean on clean method signatures as documentation. We may change this later.
Style/Documentation:
Enabled: false

# Sometimes we leave comments in empty else statements intentionally
Style/EmptyElse:
Enabled: false

# Sometimes we want to more explicitly list out a condition
Style/RedundantCondition:
Enabled: false

# This leads to code that is not very readable at times (very long lines)
Layout/MultilineMethodCallIndentation:
Enabled: false

# Blocks across lines are okay sometimes
Style/BlockDelimiters:
Enabled: false

# This leads to code that is not very readable at times (very long lines)
Layout/FirstArgumentIndentation:
Enabled: false

# This leads to code that is not very readable at times (very long lines)
Layout/ArgumentAlignment:
Enabled: false

Style/AccessorGrouping:
Enabled: false

Style/NumericPredicate:
Enabled: false

Layout/BlockEndNewline:
Enabled: false
21 changes: 20 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
packs (0.0.1)
packs (0.0.2)
sorbet-runtime

GEM
Expand All @@ -10,6 +10,7 @@ GEM
ast (2.4.2)
coderay (1.1.3)
diff-lcs (1.5.0)
json (2.6.3)
method_source (1.0.0)
netrc (0.11.0)
parallel (1.22.1)
Expand All @@ -18,12 +19,15 @@ GEM
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
rbi (0.0.16)
ast
parser (>= 2.6.4.0)
sorbet-runtime (>= 0.5.9204)
unparser
regexp_parser (2.6.1)
rexml (3.2.5)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
Expand All @@ -37,6 +41,19 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-support (3.11.1)
rubocop (1.41.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.23.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.24.0)
parser (>= 3.1.1.0)
ruby-progressbar (1.11.0)
sorbet (0.5.10479)
sorbet-static (= 0.5.10479)
sorbet-runtime (0.5.10479)
Expand Down Expand Up @@ -68,6 +85,7 @@ GEM
thor (>= 1.2.0)
yard-sorbet
thor (1.2.1)
unicode-display_width (2.3.0)
unparser (0.6.5)
diff-lcs (~> 1.3)
parser (>= 3.1.0)
Expand All @@ -86,6 +104,7 @@ DEPENDENCIES
packs!
rake
rspec (~> 3.0)
rubocop
sorbet
tapioca

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
# packs

Welcome to `packs`! `packs` are a simple ruby specification for an extensible packaging system to help modularize Ruby applications.

A `pack` (also called `package`) is a folder of Ruby code with a `package.yml` at the root that is intended to represent a well-modularized domain, and the rest of the [rubyatscale](https://github.com/rubyatscale) ecosystem is intended to help make the boundaries between a pack and any other more clear.

Here are some example integrations with `packs`:
- [`stimpack`](https://github.com/rubyatscale/stimpack) can be used to integrate `packs` into your `rails` application
- [`rubocop-packs`](https://github.com/rubyatscale/rubocop-packs) contains cops to improve boundaries around `packs`
- [`packwerk`](https://github.com/Shopify/packwerk) and [`packwerk-extensions`](https://github.com/rubyatscale/packwerk-extensions) help you describe and constrain your package graph in terms of dependencies between packs and pack public API
- [`code_ownership`](https://github.com/rubyatscale/code_ownership) gives your application the capability to determine the owner of a pack
- [`use_packs`](https://github.com/rubyatscale/use_packs) gives a CLI, `bin/packs`, that makes it easy to create new packs, move files between packs, and more.
- [`pack_stats`](https://github.com/rubyatscale/pack_stats) makes it easy to send metrics about pack adoption and modularization to your favorite metrics provider, such as DataDog (which has built-in support).

# How is a pack different from a gem?
A ruby [`gem`](https://guides.rubygems.org/what-is-a-gem/) is the Ruby community solution for packaging and distributing Ruby code. A gem is a great place to start new projects, and a great end state for code that's been extracted from an existing codebase. `packs` are intended to help gradually modularize an application that has some conceptual boundaries, but is not yet ready to be factored into gems.
29 changes: 29 additions & 0 deletions bin/rubocop
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'rubocop' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'pathname'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
Pathname.new(__FILE__).realpath)

bundle_binstub = File.expand_path('bundle', __dir__)

if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end

require 'rubygems'
require 'bundler/setup'

load Gem.bin_path('rubocop', 'rubocop')
65 changes: 64 additions & 1 deletion lib/packs.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
# typed: strict

# Coming soon...!
require 'yaml'
require 'sorbet-runtime'
require 'packs/pack'
require 'packs/configuration'
require 'packs/private'

module Packs
PACKAGE_FILE = T.let('package.yml'.freeze, String)
ROOTS = T.let(%w[packs components], T::Array[String])

class << self
extend T::Sig

sig { returns(T::Array[Pack]) }
def all
packs_by_name.values
end

sig { params(name: String).returns(T.nilable(Pack)) }
def find(name)
packs_by_name[name]
end

sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Pack)) }
def for_file(file_path)
path_string = file_path.to_s
@for_file = T.let(@for_file, T.nilable(T::Hash[String, T.nilable(Pack)]))
@for_file ||= {}
@for_file[path_string] ||= all.find { |package| path_string.start_with?("#{package.name}/") || path_string == package.name }
end

sig { void }
def bust_cache!
@packs_by_name = nil
@config = nil
@for_file = nil
end

private

sig { returns(T::Hash[String, Pack]) }
def packs_by_name
@packs_by_name = T.let(@packs_by_name, T.nilable(T::Hash[String, Pack]))
@packs_by_name ||= begin
all_packs = package_glob_patterns.map do |path|
Pack.from(path)
end

# We want to match more specific paths first so for_file works correctly.
sorted_packages = all_packs.sort_by { |package| -package.name.length }
sorted_packages.to_h { |p| [p.name, p] }
end
end

sig { returns(T::Array[Pathname]) }
def package_glob_patterns
config.roots.flat_map do |root|
absolute_root = Private.root.join(root)
[
*absolute_root.glob("*/#{PACKAGE_FILE}"),
*absolute_root.glob("*/*/#{PACKAGE_FILE}")
]
end
end
end
end
37 changes: 37 additions & 0 deletions lib/packs/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# typed: strict

module Packs
class Configuration
extend T::Sig

sig { params(roots: T::Array[String]).void }
attr_writer :roots

sig { void }
def initialize
@roots = T.let(ROOTS, T::Array[String])
end

sig { returns(T::Array[Pathname]) }
def roots
@roots.map do |root|
Pathname.new(root)
end
end
end

class << self
extend T::Sig

sig { returns(Configuration) }
def config
@config = T.let(@config, T.nilable(Configuration))
@config ||= Configuration.new
end

sig { params(blk: T.proc.params(arg0: Configuration).void).void }
def configure(&blk)
yield(config)
end
end
end
43 changes: 43 additions & 0 deletions lib/packs/pack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# typed: strict

module Packs
class Pack < T::Struct
extend T::Sig

const :name, String
const :path, Pathname
const :relative_path, Pathname
const :raw_hash, T::Hash[T.untyped, T.untyped]

sig { params(package_yml_absolute_path: Pathname).returns(Pack) }
def self.from(package_yml_absolute_path)
package_loaded_yml = YAML.load_file(package_yml_absolute_path)
path = package_yml_absolute_path.dirname
relative_path = path.relative_path_from(Private.root)
package_name = relative_path.cleanpath.to_s

Pack.new(
name: package_name,
path: path,
relative_path: relative_path,
raw_hash: package_loaded_yml || {}
)
end

sig { params(relative: T::Boolean).returns(Pathname) }
def yml(relative: true)
path_to_use = relative ? relative_path : path
path_to_use.join(PACKAGE_FILE).cleanpath
end

sig { returns(String) }
def last_name
relative_path.basename.to_s
end

sig { returns(T::Hash[T.untyped, T.untyped]) }
def metadata
raw_hash['metadata'] || {}
end
end
end
Loading

0 comments on commit b4983b1

Please sign in to comment.