categoria 0.1.0.pre.beta0 → 0.1.0.pre.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 708dc304bd6e976ef1570ee99cb1208b4814c37fdcff7e0f69ac4d2ce7970e50
4
- data.tar.gz: d2db1ff75ae27757941554ee650ea86e5db8fb6a34d715a439e74b2bbc67f4b3
3
+ metadata.gz: a0660b4ae95d4760102773e4c0f7ec88dc6261a740f4edacb521a853501bbb9d
4
+ data.tar.gz: 41bdb997ca840970d69dfa8847e23217b9d742eeb3f8720ae16cb6ac84203596
5
5
  SHA512:
6
- metadata.gz: 84ff6674a0d1c147b13a449c2f816314d1e748263e67bfad5ada878055897398ee60a501d372649718d74e34d3973447553cd45747a098aa9433d6851e137a37
7
- data.tar.gz: 3581214bb48972f200f24b59567b2e529fdcf6c3164aa49f3b001d38c6173af89aa182cb240995d2eb058312d28ebeaa61225b195bc7a6150117a7b34ef4246d
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
- - `internal`: to be described
28
- - `command`: to be described
29
- - `data`: to be described
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
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Categoria
5
+ class Data
6
+ extend T::Helpers
7
+ extend T::Sig
8
+
9
+ abstract!
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Categoria
5
+ module Types
6
+ extend T::Sig
7
+
8
+ class Component < T::Enum
9
+ enums do
10
+ Command = new("command")
11
+ Model = new("model")
12
+ Data = new("data")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Categoria
5
- VERSION = "0.1.0-beta0"
5
+ VERSION = "0.1.0-beta1"
6
6
  end
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
- require_relative "categoria/version"
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; end
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
- domain_directory_path = "#{Rails.root}/app/lib/#{file_name}"
16
- %w[
17
- internal/commands
18
- internal/models
19
- command
20
- data
21
- ].each do |component_path|
22
- full_component_path = "#{domain_directory_path}/#{component_path}"
23
-
24
- empty_directory full_component_path
25
- create_file "#{full_component_path}/.keep"
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
- "#{domain_directory_path}.rb"
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
- class ModelGenerator < ::Rails::Generators::NamedBase; end
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= Rails.application.class.module_parent %>
4
+ module <%= domain_module %>
5
+ module Data
6
+ class <%= data_class_name %> < ::Categoria::Data
7
+ end
8
+ end
9
+ end
10
+ end
11
+
@@ -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.beta0
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-03 00:00:00.000000000 Z
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.txt
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
- - sig/categoria.rbs
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
@@ -1,4 +0,0 @@
1
- module Categoria
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
File without changes