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 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