rbs_activerecord 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +22 -0
  3. data/.vscode/extensions.json +6 -0
  4. data/.vscode/settings.json +12 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +49 -0
  8. data/Rakefile +14 -0
  9. data/Steepfile +9 -0
  10. data/lib/generators/rbs_activerecord/install_generator.rb +21 -0
  11. data/lib/rbs_activerecord/generator/active_storage/instance_methods.rb +57 -0
  12. data/lib/rbs_activerecord/generator/active_storage/scopes.rb +39 -0
  13. data/lib/rbs_activerecord/generator/associations.rb +118 -0
  14. data/lib/rbs_activerecord/generator/attributes.rb +95 -0
  15. data/lib/rbs_activerecord/generator/delegated_type/instance_methods.rb +66 -0
  16. data/lib/rbs_activerecord/generator/delegated_type/scopes.rb +44 -0
  17. data/lib/rbs_activerecord/generator/enum/base.rb +63 -0
  18. data/lib/rbs_activerecord/generator/enum/instance_methods.rb +51 -0
  19. data/lib/rbs_activerecord/generator/enum/scopes.rb +51 -0
  20. data/lib/rbs_activerecord/generator/pluck_overloads.rb +41 -0
  21. data/lib/rbs_activerecord/generator/scopes.rb +99 -0
  22. data/lib/rbs_activerecord/generator/secure_password.rb +54 -0
  23. data/lib/rbs_activerecord/generator.rb +143 -0
  24. data/lib/rbs_activerecord/model.rb +33 -0
  25. data/lib/rbs_activerecord/parser/evaluator.rb +48 -0
  26. data/lib/rbs_activerecord/parser/visitor.rb +67 -0
  27. data/lib/rbs_activerecord/parser.rb +30 -0
  28. data/lib/rbs_activerecord/rake_task.rb +61 -0
  29. data/lib/rbs_activerecord/utils.rb +58 -0
  30. data/lib/rbs_activerecord/version.rb +5 -0
  31. data/lib/rbs_activerecord.rb +27 -0
  32. data/rbs_collection.lock.yaml +352 -0
  33. data/rbs_collection.yaml +18 -0
  34. data/sig/generators/rbs_activerecord/install_generator.rbs +7 -0
  35. data/sig/rbs_activerecord/generator/active_storage/instance_methods.rbs +23 -0
  36. data/sig/rbs_activerecord/generator/active_storage/scopes.rbs +23 -0
  37. data/sig/rbs_activerecord/generator/associations.rbs +29 -0
  38. data/sig/rbs_activerecord/generator/attributes.rbs +28 -0
  39. data/sig/rbs_activerecord/generator/delegated_type/instance_methods.rbs +31 -0
  40. data/sig/rbs_activerecord/generator/delegated_type/scopes.rbs +26 -0
  41. data/sig/rbs_activerecord/generator/enum/base.rbs +19 -0
  42. data/sig/rbs_activerecord/generator/enum/instance_methods.rbs +26 -0
  43. data/sig/rbs_activerecord/generator/enum/scopes.rbs +26 -0
  44. data/sig/rbs_activerecord/generator/pluck_overloads.rbs +23 -0
  45. data/sig/rbs_activerecord/generator/scopes.rbs +27 -0
  46. data/sig/rbs_activerecord/generator/secure_password.rbs +22 -0
  47. data/sig/rbs_activerecord/generator.rbs +34 -0
  48. data/sig/rbs_activerecord/model.rbs +26 -0
  49. data/sig/rbs_activerecord/parser/evaluator.rbs +12 -0
  50. data/sig/rbs_activerecord/parser/visitor.rbs +27 -0
  51. data/sig/rbs_activerecord/parser.rbs +15 -0
  52. data/sig/rbs_activerecord/rake_task.rbs +19 -0
  53. data/sig/rbs_activerecord/utils.rbs +14 -0
  54. data/sig/rbs_activerecord/version.rbs +5 -0
  55. data/sig/rbs_activerecord.rbs +6 -0
  56. metadata +142 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b4083cdfd1907ddd9ba845bf3535ed264504753c6fe80c08ac723891a0c990aa
4
+ data.tar.gz: 8825d942ba46a8b35cd4aa3d57ef723aa7fbc51333d7fe30bdca1b143ed73693
5
+ SHA512:
6
+ metadata.gz: '07838fda26b2d56872fa91f7d3d9a89b2eb89c5c93591722547ca27af65d65ff72ae1703c8366d4c556559d18cbcc8668930c8c4e2bbb7c89aaaa3e1b7ec84a3'
7
+ data.tar.gz: 605694bc6458aa4f6076d89add3a162c66edceefd357f2402e989a093f898b853f1d363cf7d121a4e5e49befa4747bd99c2ae9a52f2a89502f25361ee1865ccf
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Layout/LeadingCommentSpace:
5
+ AllowRBSInlineAnnotation: true
6
+ AllowSteepAnnotation: true
7
+
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - "spec/**/*.rb"
11
+
12
+ Metrics/MethodLength:
13
+ Max: 30
14
+
15
+ Style/Documentation:
16
+ Enabled: false
17
+
18
+ Style/StringLiterals:
19
+ EnforcedStyle: double_quotes
20
+
21
+ Style/StringLiteralsInInterpolation:
22
+ EnforcedStyle: double_quotes
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "shopify.ruby-lsp",
4
+ "tk0miya.rbs-helper"
5
+ ]
6
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "[ruby]": {
3
+ "editor.defaultFormatter": "Shopify.ruby-lsp"
4
+ },
5
+ "rbs-helper.rbs-inline-signature-directory": "sig/",
6
+ "rbs-helper.rbs-inline-on-save": true,
7
+ "cSpell.words": [
8
+ "activerecord",
9
+ "codebases",
10
+ "Cyclomatic"
11
+ ]
12
+ }
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Takeshi KOMIYA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # rbs_activerecord
2
+
3
+ rbs_activerecord is a RBSGenerator for models built with ActiveRecord.
4
+
5
+ ## Installation
6
+
7
+ Add a new entry to your Gemfile and run `bundle install`:
8
+
9
+ group :development do
10
+ gem 'rbs_activerecord', require: false
11
+ end
12
+
13
+ After the installation, please run rake task generator:
14
+
15
+ bundle exec rails g rbs_activerecord:install
16
+
17
+ ## Usage
18
+
19
+ Run `rbs:activerecord:setup` task:
20
+
21
+ bundle exec rake rbs:activerecord:setup
22
+
23
+ Then rbs_activerecord will scan your code and generate RBS files into
24
+ `sig/activerecord` directory.
25
+
26
+ ## Development
27
+
28
+ After checking out the repo, run `bin/setup` to install dependencies. You can also
29
+ run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`.
32
+ To release a new version, update the version number in `version.rb`, and then put
33
+ a git tag (ex. `git tag v1.0.0`) and push it to the GitHub. Then GitHub Actions
34
+ will release a new package to [rubygems.org](https://rubygems.org) automatically.
35
+
36
+ ## Contributing
37
+
38
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tk0miya/rbs_activerecord.
39
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are
40
+ expected to adhere to the [code of conduct](https://github.com/tk0miya/rbs_activerecord/blob/main/CODE_OF_CONDUCT.md).
41
+
42
+ ## License
43
+
44
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
45
+
46
+ ## Code of Conduct
47
+
48
+ Everyone interacting in the rbs_activerecord project's codebases, issue trackers is expected to
49
+ follow the [code of conduct](https://github.com/tk0miya/rbs_activerecord/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
9
+
10
+ namespace :rbs do
11
+ task :generate do
12
+ sh "rbs-inline", "--opt-out", "--output=sig", "lib"
13
+ end
14
+ end
data/Steepfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # D = Steep::Diagnostic
4
+
5
+ target :lib do
6
+ signature "sig"
7
+
8
+ check "lib"
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module RbsActiverecord
6
+ class InstallGenerator < Rails::Generators::Base
7
+ def create_raketask
8
+ create_file "lib/tasks/rbs_activerecord.rake", <<~RUBY
9
+ # frozen_string_literal: true
10
+
11
+ begin
12
+ require "rbs_activerecord/rake_task"
13
+
14
+ RbsActiverecord::RakeTask.new
15
+ rescue LoadError
16
+ # failed to load rbs_activerecord. Skip to load rbs_activerecord tasks.
17
+ end
18
+ RUBY
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsActiverecord
4
+ class Generator
5
+ module ActiveStorage
6
+ class InstanceMethods
7
+ attr_reader :model #: RbsActiverecord::Model
8
+
9
+ # @rbs model: RbsActiverecord::Model
10
+ def initialize(model) #: void
11
+ @model = model
12
+ end
13
+
14
+ def generate #: String
15
+ <<~RBS.strip
16
+ module GeneratedActiveStorageInstanceMethods
17
+ #{attachments.map { |name, reflection| attachment(name, reflection) }.join("\n")}
18
+ end
19
+ RBS
20
+ end
21
+
22
+ private
23
+
24
+ def attachments #: Hash[String, untyped]
25
+ if model.klass.respond_to?(:attachment_reflections)
26
+ model.klass.attachment_reflections # steep:ignore
27
+ else
28
+ {}
29
+ end
30
+ end
31
+
32
+ # @rbs name: String
33
+ def attachment(name, reflection) #: String
34
+ case reflection.macro
35
+ when :has_one_attached
36
+ <<~RBS
37
+ def #{name}: () -> ::ActiveStorage::Attached::One
38
+ def #{name}=: (::ActionDispatch::Http::UploadedFile) -> ::ActionDispatch::Http::UploadedFile
39
+ | (::Rack::Test::UploadedFile) -> ::Rack::Test::UploadedFile
40
+ | (::ActiveStorage::Blob) -> ::ActiveStorage::Blob
41
+ | (::String) -> ::String
42
+ | ({ io: ::IO, filename: ::String, content_type: ::String? }) -> { io: ::IO, filename: ::String, content_type: ::String? }
43
+ | (nil) -> nil
44
+ RBS
45
+ when :has_many_attached
46
+ <<~RBS
47
+ def #{name}: () -> ::ActiveStroage::Attached::Many
48
+ def #{name}=: (untyped) -> untyped
49
+ RBS
50
+ else
51
+ raise "unknown macro: #{reflection.macro}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsActiverecord
4
+ class Generator
5
+ module ActiveStorage
6
+ class Scopes
7
+ attr_reader :model #: RbsActiverecord::Model
8
+
9
+ # @rbs model: RbsActiverecord::Model
10
+ def initialize(model) #: void
11
+ @model = model
12
+ end
13
+
14
+ def generate #: String
15
+ <<~RBS.strip
16
+ module GeneratedActiveStorageScopeMethods[Relation]
17
+ #{attachments.map { |name, _| attachment(name) }.join("\n")}
18
+ end
19
+ RBS
20
+ end
21
+
22
+ private
23
+
24
+ def attachments #: Hash[String, untyped]
25
+ if model.klass.respond_to?(:attachment_reflections)
26
+ model.klass.attachment_reflections # steep:ignore
27
+ else
28
+ {}
29
+ end
30
+ end
31
+
32
+ # @rbs name: String
33
+ def attachment(name) #: String
34
+ "def with_attached_#{name}: () -> Relation"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsActiverecord
4
+ class Generator
5
+ class Associations
6
+ include Utils
7
+
8
+ attr_reader :model #: RbsActiverecord::Model
9
+
10
+ # @rbs model: RbsActiverecord::Model
11
+ def initialize(model) #: void
12
+ @model = model
13
+ end
14
+
15
+ def generate #: String
16
+ <<~RBS.strip
17
+ module GeneratedAssociationMethods
18
+ #{has_many}
19
+ #{has_one}
20
+ #{belongs_to}
21
+ #{has_and_belongs_to_many}
22
+ end
23
+ RBS
24
+ end
25
+
26
+ private
27
+
28
+ def has_many #: String # rubocop:disable Naming/PredicateName
29
+ model.reflect_on_all_associations(:has_many).map do |assoc|
30
+ assoc_name = assoc.name.to_s
31
+ klass_name = assoc.klass.name
32
+ collection = "#{klass_name}::ActiveRecord_Associations_CollectionProxy"
33
+ primary_key_type = primary_key_type_for(assoc.klass)
34
+
35
+ <<~RBS
36
+ def #{assoc_name}: () -> #{collection}
37
+ def #{assoc_name}=: (#{collection} | Array[::#{klass_name}]) -> (#{collection} | Array[::#{klass_name}])
38
+ def #{assoc_name.singularize}_ids: () -> Array[#{primary_key_type}]
39
+ def #{assoc_name.singularize}_ids=: (Array[#{primary_key_type}]) -> Array[#{primary_key_type}]
40
+ RBS
41
+ end.join("\n")
42
+ end
43
+
44
+ def has_one #: String # rubocop:disable Naming/PredicateName
45
+ model.reflect_on_all_associations(:has_one).map do |assoc|
46
+ type = assoc.klass.name
47
+ optional = "#{type}?"
48
+
49
+ <<~RBS
50
+ def #{assoc.name}: () -> #{optional}
51
+ def #{assoc.name}=: (#{optional}) -> #{optional}
52
+ def build_#{assoc.name}: (?untyped) -> #{type}
53
+ def create_#{assoc.name}: (untyped) -> #{type}
54
+ def create_#{assoc.name}!: (untyped) -> #{type}
55
+ def reload_#{assoc.name}: () -> #{optional}
56
+ def reset_#{assoc.name}: () -> void
57
+ def #{assoc.name}_changed?: () -> bool
58
+ def #{assoc.name}_previously_changed?: () -> bool
59
+ RBS
60
+ end.join("\n")
61
+ end
62
+
63
+ def belongs_to #: String # rubocop:disable Metrics/AbcSize
64
+ model.reflect_on_all_associations(:belongs_to).map do |assoc|
65
+ is_optional = assoc.options[:optional]
66
+ type = assoc.polymorphic? ? polymorphic_owner_types(assoc) : assoc.klass.name
67
+ optional = "#{type}?"
68
+
69
+ # @type var methods: Array[String]
70
+ methods = []
71
+ methods << "def #{assoc.name}: () -> #{is_optional ? optional : type}"
72
+ methods << "def #{assoc.name}=: (#{optional}) -> #{optional}"
73
+ unless assoc.polymorphic?
74
+ methods << "def build_#{assoc.name}: (untyped) -> #{type}"
75
+ methods << "def create_#{assoc.name}: (untyped) -> #{type}"
76
+ methods << "def create_#{assoc.name}!: (untyped) -> #{type}"
77
+ end
78
+ methods << "def reload_#{assoc.name}: () -> #{optional}"
79
+ methods << "def reset_#{assoc.name}: () -> void"
80
+ methods.join("\n")
81
+ end.join("\n")
82
+ end
83
+
84
+ # @rbs assoc: untyped
85
+ def polymorphic_owner_types(assoc) #: String # rubocop:disable Metrics/AbcSize
86
+ table_name = model.klass.name.to_s.tableize.to_sym
87
+ owners = ActiveRecord::Base.descendants.select do |klass|
88
+ klass.reflect_on_all_associations.any? { |a| a.name == table_name && a.options[:as] == assoc.name }
89
+ end
90
+
91
+ if owners.empty?
92
+ "untyped"
93
+ elsif owners.size == 1
94
+ owners.first.name
95
+ else
96
+ names = owners.map(&:name).sort.join(" | ")
97
+ "(#{names})"
98
+ end
99
+ end
100
+
101
+ def has_and_belongs_to_many #: String # rubocop:disable Naming/PredicateName
102
+ model.reflect_on_all_associations(:has_and_belongs_to_many).map do |assoc|
103
+ assoc_name = assoc.name.to_s
104
+ klass_name = assoc.klass.name
105
+ collection = "#{klass_name}::ActiveRecord_Associations_CollectionProxy"
106
+ primary_key_type = primary_key_type_for(assoc.klass)
107
+
108
+ <<~RBS
109
+ def #{assoc_name}: () -> #{collection}
110
+ def #{assoc_name}=: (#{collection} | Array[::#{klass_name}]) -> (#{collection} | Array[::#{klass_name}])
111
+ def #{assoc_name.singularize}_ids: () -> Array[#{primary_key_type}]
112
+ def #{assoc_name.singularize}_ids=: (Array[#{primary_key_type}]) -> Array[#{primary_key_type}]
113
+ RBS
114
+ end.join("\n")
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsActiverecord
4
+ class Generator
5
+ class Attributes
6
+ include Utils
7
+
8
+ attr_reader :model #: RbsActiverecord::Model
9
+
10
+ # @rbs model: RbsActiverecord::Model
11
+ def initialize(model) #: void
12
+ @model = model
13
+ end
14
+
15
+ def generate #: String
16
+ <<~RBS
17
+ module GeneratedAttributeMethods
18
+ #{model.columns.map { |c| column(c) }.join("\n")}
19
+ #{attributes.map { |name, type| attribute(name, type) }.join("\n")}
20
+ #{model.attribute_aliases.map { |from, to| alias_method(from, to) }.join("\n")}
21
+ end
22
+ RBS
23
+ end
24
+
25
+ private
26
+
27
+ def column(col) #: String # rubocop:disable Metrics/AbcSize
28
+ type = sql_type_to_class(col.type)
29
+ optional = "#{type}?"
30
+ column_type = col.null ? optional : type
31
+
32
+ <<~RBS
33
+ def #{col.name}: () -> #{column_type}
34
+ def #{col.name}=: (#{column_type}) -> #{column_type}
35
+ def #{col.name}?: () -> bool
36
+ def #{col.name}_changed?: () -> bool
37
+ def #{col.name}_change: () -> [#{optional}, #{optional}]
38
+ def #{col.name}_will_change!: () -> void
39
+ def #{col.name}_was: () -> #{optional}
40
+ def #{col.name}_previously_changed?: () -> bool
41
+ def #{col.name}_previous_change: () -> ::Array[#{optional}]?
42
+ def #{col.name}_previously_was: () -> #{optional}
43
+ def #{col.name}_before_last_save: () -> #{optional}
44
+ def #{col.name}_change_to_be_saved: () -> ::Array[#{optional}]?
45
+ def #{col.name}_in_database: () -> #{optional}
46
+ def saved_change_to_#{col.name}: () -> ::Array[#{optional}]?
47
+ def saved_change_to_#{col.name}?: () -> bool
48
+ def will_save_change_to_#{col.name}?: () -> bool
49
+ def restore_#{col.name}!: () -> void
50
+ def clear_#{col.name}_change: () -> void
51
+ RBS
52
+ end
53
+
54
+ def attributes #: Hash[String, untyped]
55
+ model.attribute_types.filter_map do |name, type|
56
+ [name, type] if model.columns.none? { |col| col.name == name }
57
+ end.to_h
58
+ end
59
+
60
+ # @rbs name: String
61
+ # @rbs type: untyped
62
+ def attribute(name, type) #: String
63
+ column_type = sql_type_to_class(type.type)
64
+
65
+ <<~RBS
66
+ def #{name}: () -> #{column_type}
67
+ def #{name}=: (#{column_type}) -> #{column_type}
68
+ RBS
69
+ end
70
+
71
+ def alias_method(from, to) #: String
72
+ <<~RBS
73
+ alias #{from} #{to}
74
+ alias #{from}= #{to}=
75
+ alias #{from}? #{to}?
76
+ alias #{from}_changed? #{to}_changed?
77
+ alias #{from}_change #{to}_change
78
+ alias #{from}_will_change! #{to}_will_change!
79
+ alias #{from}_was #{to}_was
80
+ alias #{from}_previously_changed? #{to}_previously_changed?
81
+ alias #{from}_previous_change #{to}_previous_change
82
+ alias #{from}_previously_was #{to}_previously_was
83
+ alias #{from}_before_last_save #{to}_before_last_save
84
+ alias #{from}_change_to_be_saved #{to}_change_to_be_saved
85
+ alias #{from}_in_database #{to}_in_database
86
+ alias saved_change_to_#{from} saved_change_to_#{to}
87
+ alias saved_change_to_#{from}? saved_change_to_#{to}?
88
+ alias will_save_change_to_#{from}? will_save_change_to_#{to}?
89
+ alias restore_#{from}! restore_#{to}!
90
+ alias clear_#{from}_change clear_#{to}_change
91
+ RBS
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsActiverecord
4
+ class Generator
5
+ module DelegatedType
6
+ class InstanceMethods
7
+ include Utils
8
+
9
+ attr_reader :model #: RbsActiverecord::Model
10
+ attr_reader :declarations #: Array[Prism::CallNode]
11
+
12
+ # @rbs model: RbsActiverecord::Model
13
+ # @rbs declarations: Hash[String, Array[Prism::CallNode]]
14
+ def initialize(model, declarations) #: void
15
+ @model = model
16
+ @declarations = declarations.fetch(model.klass.name.to_s, [])
17
+ end
18
+
19
+ def generate #: String
20
+ <<~RBS.strip
21
+ module GeneratedDelegatedTypeInstanceMethods
22
+ #{delegated_types.map { |node| delegated_type(node) }.join("\n")}
23
+ end
24
+ RBS
25
+ end
26
+
27
+ private
28
+
29
+ def delegated_types #: Array[Prism::CallNode]
30
+ declarations.select { |node| node.name == :delegated_type }
31
+ end
32
+
33
+ # @rbs node: Prism::CallNode
34
+ def delegated_type(node) #: String
35
+ arguments = node.arguments&.arguments || []
36
+ name, options = arguments.map { |arg| Parser.eval_node(arg) } #: [String?, Hash[Symbol, untyped]]
37
+ return "" unless name
38
+
39
+ types = options[:types]
40
+ role_methods = <<~RBS
41
+ def #{name}_class: () -> (#{types.join(" | ")})
42
+ def #{name}_name: () -> String
43
+ RBS
44
+
45
+ type_methods = types.map do |type|
46
+ <<~RBS
47
+ def #{type.underscore}?: () -> bool
48
+ def #{type.underscore}: () -> #{type}?
49
+ def #{type.underscore}_id: () -> #{primary_key_type_for(type)}?
50
+ RBS
51
+ end.join("\n")
52
+
53
+ role_methods + type_methods
54
+ end
55
+
56
+ # @rbs klass_name: String
57
+ def primary_key_type_for(klass_name) #: String
58
+ klass = Object.const_get(klass_name)
59
+ super(klass)
60
+ rescue NameError
61
+ "::Integer"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end