categoria 0.1.0.pre.beta0 → 0.1.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +25 -4
- data/lib/categoria/data.rb +11 -0
- data/lib/categoria/types.rb +16 -0
- data/lib/categoria/version.rb +1 -1
- data/lib/categoria.rb +5 -1
- data/lib/generators/categoria/command_generator.rb +33 -1
- data/lib/generators/categoria/data_generator.rb +18 -0
- data/lib/generators/categoria/domain_generator.rb +37 -13
- data/lib/generators/categoria/helpers.rb +63 -0
- data/lib/generators/categoria/initializer_generator.rb +24 -0
- data/lib/generators/categoria/model_generator.rb +71 -1
- data/lib/generators/categoria/templates/data.rb.erb +11 -0
- data/lib/generators/categoria/templates/domain_description.yml.erb +12 -0
- data/lib/generators/categoria/templates/model.rb.erb +13 -0
- data/lib/generators/categoria/templates/model_migration.rb.erb +28 -0
- data/lib/tasks/check.rake +0 -0
- metadata +13 -4
- data/sig/categoria.rbs +0 -4
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0660b4ae95d4760102773e4c0f7ec88dc6261a740f4edacb521a853501bbb9d
|
4
|
+
data.tar.gz: 41bdb997ca840970d69dfa8847e23217b9d742eeb3f8720ae16cb6ac84203596
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b75fcff2064e3150c32885e9ec65c6035fc6a0b12d9223596627a1f74f80ee380839badd568a88412bd0d095be5494e7d3de873721f83ec8ad02beddeb04496c
|
7
|
+
data.tar.gz: aa8228b9561687786378d77fe84939b02a8e16d80482e686c5e6491eba43453764378803ce6ba5c78e30fec4ac55a95bfbd4807d2a2d0fd55c7cf00d6bdd6dfe
|
data/README.md
CHANGED
@@ -17,16 +17,37 @@ along domain lines. it's similar in spirit to [phoenix's context][phxc]. the
|
|
17
17
|
point of departure is the following:
|
18
18
|
|
19
19
|
**a category/domain doesn't concern itself with the web part of the application.**
|
20
|
+
that is because the web concerns tend to be cross-cutting (across domains). in a
|
21
|
+
controller action, it's possible to invoke several commands from different
|
22
|
+
domains in service of the request. the returned response might also be a
|
23
|
+
combination of data from different domains. in order to allow this freedom, all
|
24
|
+
app components that directly handle web requests can remain in their
|
25
|
+
conventional locations.
|
26
|
+
|
27
|
+
what to call controllers? typically they have matched a given model. you have a
|
28
|
+
`Document` model, here's the `DocumentsController`. now that the document model
|
29
|
+
is subsumed under a domain of different name, the controller should be allowed
|
30
|
+
to float freely. it could be an interface to a domain, or to several domains at
|
31
|
+
the same time. an appropriate name should be chosen.
|
20
32
|
|
21
33
|
### directory structure of a domain
|
22
34
|
|
23
35
|
domains live under `app/lib` of the rails application. internally, it is
|
24
36
|
organized into three main categories, represented by directories and ruby module
|
25
|
-
namespaces
|
37
|
+
namespaces. all domains are sub-namespaced under the application's main
|
38
|
+
namespace. an initializer is added to override zeitwerk's default behavior of
|
39
|
+
using `Object` as the root namespace for classes loaded from `app/lib`.
|
26
40
|
|
27
|
-
|
28
|
-
|
29
|
-
|
41
|
+
#### `internal`
|
42
|
+
#### `command`
|
43
|
+
#### `data`
|
44
|
+
|
45
|
+
## Test
|
46
|
+
|
47
|
+
the same structure is repeated under the `test` directory, under universal
|
48
|
+
`Test` namespace. just so constant loading, in non-test local and remote (prod)
|
49
|
+
environments don't load unnecessary code. the `Test` namespace should only be
|
50
|
+
loaded in a test environment.
|
30
51
|
|
31
52
|
[discourse]: https:/github.com/discourse/discourse
|
32
53
|
[dc_models]: https://github.com/discourse/discourse/tree/main/app/models
|
data/lib/categoria/version.rb
CHANGED
data/lib/categoria.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "zeitwerk"
|
4
5
|
require "sorbet-runtime"
|
5
6
|
|
6
|
-
|
7
|
+
loader = Zeitwerk::Loader.for_gem
|
8
|
+
loader.ignore("#{__dir__}/generators")
|
9
|
+
loader.setup
|
7
10
|
|
8
11
|
module Categoria
|
12
|
+
extend T::Sig
|
9
13
|
end
|
@@ -1,8 +1,40 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require_relative "helpers"
|
5
|
+
|
4
6
|
module Categoria
|
5
7
|
module Generators
|
6
|
-
class CommandGenerator < ::Rails::Generators::NamedBase
|
8
|
+
class CommandGenerator < ::Rails::Generators::NamedBase
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include Helpers
|
12
|
+
|
13
|
+
Component = Types::Component
|
14
|
+
|
15
|
+
def generate_command_class
|
16
|
+
in_root do
|
17
|
+
component_path = domain_component_path(domain, Component::Command)
|
18
|
+
class_path = component_path.join("#{component}.rb")
|
19
|
+
|
20
|
+
create_file class_path, <<~COMMAND
|
21
|
+
# frozen_string_literal: true
|
22
|
+
|
23
|
+
module #{root_module}
|
24
|
+
module #{domain_module}
|
25
|
+
module Command
|
26
|
+
class #{class_name}
|
27
|
+
def self.invoke(arg0); end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
COMMAND
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { returns(String) }
|
37
|
+
def class_name = component.classify
|
38
|
+
end
|
7
39
|
end
|
8
40
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Categoria
|
5
|
+
module Generators
|
6
|
+
class DataGenerator < ::Rails::Generators::NamedBase
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
desc <<~DOC.squish
|
12
|
+
generates a data class for the domain. data classes are used
|
13
|
+
like value classes: hashes are more fit for the purpose except
|
14
|
+
that in the wild hashes are difficult to tame.
|
15
|
+
DOC
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -8,27 +8,51 @@ module Categoria
|
|
8
8
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
10
10
|
|
11
|
+
sig { returns(String) }
|
11
12
|
def module_name = file_name.capitalize
|
12
13
|
|
13
14
|
sig { void }
|
14
15
|
def setup_new_domain
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
16
|
+
domain_dir = Pathname.new("app/lib/#{file_name}")
|
17
|
+
|
18
|
+
in_root do
|
19
|
+
# turns out that in order to cleanly undo the changes
|
20
|
+
# introduced by the generator, we should make a few of
|
21
|
+
# the actions more explicit, and independent, like the
|
22
|
+
# creation of the domain directory itself.
|
23
|
+
empty_directory domain_dir
|
24
|
+
|
25
|
+
%w[
|
26
|
+
internal/commands
|
27
|
+
internal/models
|
28
|
+
command
|
29
|
+
data
|
30
|
+
].each do |component_path|
|
31
|
+
create_empty_directory_with_keep_file \
|
32
|
+
at: domain_dir.join(component_path)
|
33
|
+
end
|
34
|
+
|
35
|
+
template \
|
36
|
+
"domain_description.yml.erb",
|
37
|
+
domain_dir.join("description.yml")
|
38
|
+
create_file domain_dir.join("README.md"), <<~README
|
39
|
+
# About #{module_name}
|
40
|
+
|
41
|
+
Describe the domain. You may provide usage guide for your public interfaces (commands).
|
42
|
+
Feel free to describe explicitly what may be implicit (e.g. triggered jobs, emitted
|
43
|
+
events, sent emails, etc).
|
44
|
+
README
|
26
45
|
end
|
27
46
|
|
28
|
-
create_file "#{domain_directory_path}/description.yml"
|
29
47
|
template \
|
30
48
|
"domain_module.rb.erb",
|
31
|
-
"#{
|
49
|
+
"#{domain_dir}.rb"
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(at: Pathname).void }
|
53
|
+
private def create_empty_directory_with_keep_file(at:)
|
54
|
+
empty_directory at
|
55
|
+
create_file at.join(".keep")
|
32
56
|
end
|
33
57
|
end
|
34
58
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "active_support/inflector"
|
5
|
+
|
6
|
+
module Categoria
|
7
|
+
module Generators
|
8
|
+
module Helpers
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
Component = Types::Component
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
def root_module = Rails.application.class.module_parent.name
|
15
|
+
|
16
|
+
sig { returns(String) }
|
17
|
+
def domain_module = domain.capitalize
|
18
|
+
|
19
|
+
sig { returns(String) }
|
20
|
+
def domain_prefix = domain.singularize
|
21
|
+
|
22
|
+
def class_name = ActiveSupport::Inflector.classify(component)
|
23
|
+
|
24
|
+
sig { returns(String) }
|
25
|
+
def domain
|
26
|
+
@domain ||= T.let(
|
27
|
+
T.must(domain_and_component[0]),
|
28
|
+
T.nilable(String)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def component
|
33
|
+
@component ||= T.let(
|
34
|
+
T.must(domain_and_component[1]),
|
35
|
+
T.nilable(String)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { returns([String, String]) }
|
40
|
+
def domain_and_component
|
41
|
+
@domain_and_component ||= T.let(
|
42
|
+
T.must(name.split(/:/)[..1]),
|
43
|
+
[String, String]
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(domain: String, component: Types::Component).returns(Pathname) }
|
48
|
+
def domain_component_path(domain, component)
|
49
|
+
domain_path = domain_path_in_root(domain)
|
50
|
+
|
51
|
+
case component
|
52
|
+
when Component::Command then Pathname.new %(#{domain_path}/command)
|
53
|
+
when Component::Model then Pathname.new %(#{domain_path}/internal/models)
|
54
|
+
when Component::Data then Pathname.new %(#{domain_path}/data)
|
55
|
+
else T.absurd(component)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(domain: String).returns(String) }
|
60
|
+
def domain_path_in_root(domain) = %(app/lib/#{domain})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Categoria
|
5
|
+
module Generators
|
6
|
+
class InitializerGenerator < ::Rails::Generators::Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { void }
|
10
|
+
def create_categoria_initializer_file
|
11
|
+
in_root do
|
12
|
+
create_file "config/initializers/categoria.rb", <<~INITIALIZER
|
13
|
+
# frozen_string_literal: true
|
14
|
+
|
15
|
+
Rails.autoloaders.main.push_dir(
|
16
|
+
"\#{Rails.root}/app/lib",
|
17
|
+
namespace: Rails.application.class.module_parent
|
18
|
+
)
|
19
|
+
INITIALIZER
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,8 +1,78 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "rails/generators"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
require "rails/generators/active_record/migration"
|
7
|
+
require "rails/generators/model_helpers"
|
8
|
+
require "rails/generators/migration"
|
9
|
+
require "active_record"
|
10
|
+
require_relative "helpers"
|
11
|
+
|
4
12
|
module Categoria
|
5
13
|
module Generators
|
6
|
-
|
14
|
+
# `ModelGenerator` is almost an exact copy of the ActiveRecord
|
15
|
+
# model generator. significant departures are as follows:
|
16
|
+
# - migrations cannot be skipped since a model should be an orm class,
|
17
|
+
# otherwise a data class is what you need. a data class typically
|
18
|
+
# serializes one or more model records into a form that is publicized.
|
19
|
+
# they are allowed to perform all sorts of key & value transformations
|
20
|
+
# for purposes of data interchange format and/or
|
21
|
+
# security-by-obscurity.
|
22
|
+
# - models are not in `app/models` but instead internal to domain.
|
23
|
+
class ModelGenerator < ::Rails::Generators::NamedBase
|
24
|
+
extend T::Sig
|
25
|
+
include ::Rails::Generators::ModelHelpers
|
26
|
+
include ::Rails::Generators::Migration
|
27
|
+
include ::ActiveRecord::Generators::Migration
|
28
|
+
include Helpers
|
29
|
+
|
30
|
+
Component = Types::Component
|
31
|
+
|
32
|
+
desc <<~DOC.squish
|
33
|
+
DOC
|
34
|
+
|
35
|
+
|
36
|
+
source_root File.expand_path("templates", __dir__)
|
37
|
+
|
38
|
+
argument \
|
39
|
+
:attributes,
|
40
|
+
type: :array,
|
41
|
+
default: [],
|
42
|
+
banner: %(field[:type][:index] field[:type][:index])
|
43
|
+
|
44
|
+
sig { void }
|
45
|
+
def create_migration_file
|
46
|
+
migrate_dir = Pathname.new(db_migrate_path)
|
47
|
+
|
48
|
+
migration_template(
|
49
|
+
"model_migration.rb.erb",
|
50
|
+
migrate_dir.join("create_#{domain_prefixed_relation_name}_table.rb"),
|
51
|
+
migration_version:
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { returns(String) }
|
56
|
+
def migration_version = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
57
|
+
|
58
|
+
sig { void }
|
59
|
+
def generate_internal_model
|
60
|
+
component_path = domain_component_path(domain, Component::Model)
|
61
|
+
class_path = component_path.join("#{component}.rb")
|
62
|
+
|
63
|
+
in_root { template "model.rb.erb", class_path }
|
64
|
+
end
|
65
|
+
|
66
|
+
sig { returns(String) }
|
67
|
+
def domain_prefixed_relation_name = %(#{domain_prefix}_#{component.pluralize})
|
68
|
+
|
69
|
+
sig { returns(T::Array[String]) }
|
70
|
+
def attributes_with_index = attributes.select { !_1.reference? && _1.has_index? }
|
71
|
+
|
72
|
+
sig { params(dirname: String).returns(String) }
|
73
|
+
def self.next_migration_number(dirname)
|
74
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
75
|
+
end
|
76
|
+
end
|
7
77
|
end
|
8
78
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Domains are typically owned by one team (and one team can own)
|
2
|
+
# many domains. The contents of this yaml file help other teams
|
3
|
+
# quickly learn about the domain, and figure out how to contact
|
4
|
+
# the owners.
|
5
|
+
---
|
6
|
+
|
7
|
+
manifest:
|
8
|
+
owner:
|
9
|
+
github_username: "@owner-github-username"
|
10
|
+
slack_channel: "#owner-slack-channel"
|
11
|
+
email: "owner@example.org"
|
12
|
+
team: "org-team-name"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module <%= Rails.application.class.module_parent %>
|
4
|
+
module <%= domain_module %>
|
5
|
+
module Internal
|
6
|
+
module Models
|
7
|
+
class <%= class_name %> < ApplicationRecord
|
8
|
+
self.table_name = "<%= domain_prefixed_relation_name %>"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
|
+
def change
|
5
|
+
create_table :<%= domain_prefixed_relation_name %><%= primary_key_type %> do |t|
|
6
|
+
<% attributes.each do |attribute| -%>
|
7
|
+
<% if attribute.password_digest? -%>
|
8
|
+
t.string :password_digest<%= attribute.inject_options %>
|
9
|
+
<% elsif attribute.token? -%>
|
10
|
+
t.string :<%= attribute.name %><%= attribute.inject_options %>
|
11
|
+
<% elsif attribute.reference? -%>
|
12
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
|
13
|
+
<% elsif !attribute.virtual? -%>
|
14
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
15
|
+
<% end -%>
|
16
|
+
<% end -%>
|
17
|
+
<% if options[:timestamps] %>
|
18
|
+
t.timestamps
|
19
|
+
<% end -%>
|
20
|
+
end
|
21
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
22
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
|
23
|
+
<% end -%>
|
24
|
+
<% attributes_with_index.each do |attribute| -%>
|
25
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
26
|
+
<% end -%>
|
27
|
+
end
|
28
|
+
end
|
File without changes
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: categoria
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.pre.
|
4
|
+
version: 0.1.0.pre.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yaw Boakye
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sorbet-runtime
|
@@ -203,17 +203,26 @@ files:
|
|
203
203
|
- ".rubocop.yml"
|
204
204
|
- ".ruby-version"
|
205
205
|
- CHANGELOG.md
|
206
|
-
- LICENSE
|
206
|
+
- LICENSE
|
207
207
|
- README.md
|
208
208
|
- Rakefile
|
209
209
|
- categoria.gemspec
|
210
210
|
- lib/categoria.rb
|
211
|
+
- lib/categoria/data.rb
|
212
|
+
- lib/categoria/types.rb
|
211
213
|
- lib/categoria/version.rb
|
212
214
|
- lib/generators/categoria/command_generator.rb
|
215
|
+
- lib/generators/categoria/data_generator.rb
|
213
216
|
- lib/generators/categoria/domain_generator.rb
|
217
|
+
- lib/generators/categoria/helpers.rb
|
218
|
+
- lib/generators/categoria/initializer_generator.rb
|
214
219
|
- lib/generators/categoria/model_generator.rb
|
220
|
+
- lib/generators/categoria/templates/data.rb.erb
|
221
|
+
- lib/generators/categoria/templates/domain_description.yml.erb
|
215
222
|
- lib/generators/categoria/templates/domain_module.rb.erb
|
216
|
-
-
|
223
|
+
- lib/generators/categoria/templates/model.rb.erb
|
224
|
+
- lib/generators/categoria/templates/model_migration.rb.erb
|
225
|
+
- lib/tasks/check.rake
|
217
226
|
- sorbet/config
|
218
227
|
- sorbet/rbi/annotations/.gitattributes
|
219
228
|
- sorbet/rbi/annotations/actionmailer.rbi
|
data/sig/categoria.rbs
DELETED
/data/{LICENSE.txt → LICENSE}
RENAMED
File without changes
|