Module: ArchSpec::DSL::Context

Defined in:
lib/archspec/dsl.rb

Overview

The top-level DSL. Declare the project, its components, an architecture preset, and global rules.

root "."
source "app/**/*.rb", "lib/**/*.rb"
ignore "app/legacy/**/*.rb"
todo "archspec_todo.yml"

component :models, in: "app/models/**/*.rb"
component :controllers, in: "app/controllers/**/*.rb"
models.cannot_use :controllers

Declaring a component defines a reader for it, so models and controllers above return an ArchSpec::DSL::ComponentProxy you attach rules to.

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name) ⇒ Object



155
156
157
158
159
# File 'lib/archspec/dsl.rb', line 155

def method_missing(name, ...)
  return ComponentProxy.new(self, name) if component?(name)

  super
end

Instance Method Details

#architecture(name, **options) ⇒ Object

Applies a bundled architecture preset, defining its components and rules together.

architecture :rails
architecture :hexagonal
architecture :modular_monolith, components: { ... }, allow: { ... }

See ArchSpec::Architectures for every preset and its options.



121
122
123
# File 'lib/archspec/dsl.rb', line 121

def architecture(name, **options)
  Architectures.apply(name, self, **options)
end

#component(name, in: nil, namespace: nil, constants: nil) ⇒ Object Also known as: layer, role

Declares a component: a named set of files, matched by glob, namespace, or explicit constant.

component :services, in: "app/services/**/*.rb"
component :billing, namespace: "Billing"
component :legacy, constants: %w[OldReport OldExport]

Returns an ArchSpec::DSL::ComponentProxy for attaching rules. The component is also available by name later in the file.

layer and role are aliases. Use whichever word fits the architecture you are describing.



103
104
105
106
107
108
# File 'lib/archspec/dsl.rb', line 103

def component(name, in: nil, namespace: nil, constants: nil)
  add_component(
    ComponentSpec.new(name, files: binding.local_variable_get(:in), namespace: namespace, constants: constants)
  )
  ComponentProxy.new(self, name)
end

#each_directory(glob) ⇒ Object

Yields each subdirectory matching a glob, so you can declare one component per engine or pack without hardcoding their names. Paths resolve against the Archspec.rb directory, not the working directory, so it does not matter where archspec is run from.

each_directory "engines/*" do |name, path|
component name, in: "#{path}/**/*.rb"
end

Yields the directory basename and its root-relative path. Returns the [name, path] pairs when called without a block.



80
81
82
83
84
85
86
87
88
89
# File 'lib/archspec/dsl.rb', line 80

def each_directory(glob)
  base = absolute_root
  pairs = Dir.glob(File.join(base, glob)).select { |path| File.directory?(path) }.sort.map do |absolute|
    [File.basename(absolute), Pathname(absolute).relative_path_from(Pathname(base)).to_s]
  end

  return pairs unless block_given?

  pairs.each { |name, path| yield(name, path) }
end

#ignore(*patterns) ⇒ Object

Adds glob patterns for files to skip. Combines with the built-in ignores for .git, tmp, vendor, and node_modules.



44
45
46
# File 'lib/archspec/dsl.rb', line 44

def ignore(*patterns)
  add_ignore_patterns(patterns)
end

#inflect(map) ⇒ Object

Declares acronym inflections for Zeitwerk name checks, mirroring your config/initializers/inflections.rb.

inflect "api" => "API", "graphql" => "GraphQL"

app/models/api_client.rb then expects APIClient.



65
66
67
# File 'lib/archspec/dsl.rb', line 65

def inflect(map)
  add_inflections(map)
end

#no_cycles!(among: nil) ⇒ Object

Forbids dependency cycles between components. Pass among: to limit the check to a subset; omit it to check every declared component.

no_cycles!
no_cycles! among: %i[billing catalog shared]

Rule id: dependencies.no_cycles.



132
133
134
# File 'lib/archspec/dsl.rb', line 132

def no_cycles!(among: nil)
  add_rule(Rules::NoCyclesRule.new(among: among))
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


161
162
163
# File 'lib/archspec/dsl.rb', line 161

def respond_to_missing?(name, include_private = false)
  component?(name) || super
end

#root(path = nil) ⇒ Object

Sets or reads the project root that file patterns resolve against. Defaults to the directory of the Archspec.rb file.



29
30
31
32
33
# File 'lib/archspec/dsl.rb', line 29

def root(path = nil)
  return root_path unless path

  self.root_path = path.to_s
end

#rule(rule) ⇒ Object

Adds a custom rule object. A rule responds to id and evaluate(graph), returning ArchSpec::Diagnostic objects. Use this to extend ArchSpec with project-specific checks.



151
152
153
# File 'lib/archspec/dsl.rb', line 151

def rule(rule)
  add_rule(rule)
end

#source(*patterns) ⇒ Object

Adds glob patterns for the files ArchSpec parses. Defaults cover app, lib, packs, and engines. Component patterns are always analyzed, so most projects never need this.



38
39
40
# File 'lib/archspec/dsl.rb', line 38

def source(*patterns)
  add_source_patterns(patterns)
end

#todo(path = 'archspec_todo.yml') ⇒ Object

Points at a todo file of accepted violations. Diagnostics recorded there are subtracted from future runs, so you can adopt ArchSpec in an existing app without fixing everything first, then burn the list down.

todo "archspec_todo.yml"

Write or refresh it with archspec check --update-todo.



55
56
57
# File 'lib/archspec/dsl.rb', line 55

def todo(path = 'archspec_todo.yml')
  self.todo_path = path.to_s
end

#verify_zeitwerk_names!(*only) ⇒ Object

Checks that files define the constant their path implies under Zeitwerk. Pass globs to restrict the check to the autoloaded tree, since Rails does not autoload lib by default.

verify_zeitwerk_names!
verify_zeitwerk_names! "app/**/*.rb"

Rule id: zeitwerk.naming.



144
145
146
# File 'lib/archspec/dsl.rb', line 144

def verify_zeitwerk_names!(*only)
  add_rule(Rules::ZeitwerkNamingRule.new(only: only))
end