tapioca_dsl_compiler_store_model 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8599b4a2721338d14a30ce4662dfe880eec4b11b2cee175b4fb23f59e7057c13
4
+ data.tar.gz: e8076445bf9f432bfe2b6fc667235037c9af89b5d2f9ea64979526190fe03835
5
+ SHA512:
6
+ metadata.gz: f4f26d03579f5c9a75a6c6816562e756391bf6b99b50fb3a086e6a1dbd1e36095daa26477ed266a473802f06360704ba5c28c303a7f1817981d0ad4afd7d1445
7
+ data.tar.gz: 1306687309250806d06ef63b9b9cac7e705e96fe97a4abc53670f98b0e5aa1075c805c9c99f423e00093fcf000881c202bfb2a4d00c7fd091f33a271c820ff81
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Tapioca DSL Compiler for StoreModel
2
+
3
+ A [Tapioca](https://github.com/Shopify/tapioca) DSL compiler that generates RBI files for [StoreModel](https://github.com/DmitryTsepelev/store_model) attributes in ActiveRecord models.
4
+
5
+ StoreModel adds JSON-backed attributes to ActiveRecord models, and this gem provides Sorbet type signatures for the methods that StoreModel dynamically generates.
6
+
7
+ ## Features
8
+
9
+ - **Automatic RBI Generation**: Generates Sorbet RBI files for StoreModel attributes
10
+ - **Comprehensive Type Coverage**: Supports both single and array StoreModel types
11
+ - **Build Methods**: Generates signatures for `build_*` methods created by StoreModel
12
+ - **Tapioca Integration**: Follows Tapioca's standard compiler patterns and is automatically discovered
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'tapioca_dsl_compiler_store_model'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install tapioca_dsl_compiler_store_model
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Once installed, Tapioca will automatically discover and use the `Tapioca::Dsl::Compilers::StoreModel` compiler when generating RBI files for ActiveRecord models that use StoreModel attributes.
37
+
38
+ ### Example
39
+
40
+ Given the following StoreModel setup:
41
+
42
+ ```ruby
43
+ # app/models/user_settings.rb
44
+ class UserSettings
45
+ include StoreModel::Model
46
+
47
+ attribute :theme, :string
48
+ attribute :notifications, :boolean
49
+ attribute :language, :string
50
+ end
51
+
52
+ # app/models/preference.rb
53
+ class Preference
54
+ include StoreModel::Model
55
+
56
+ attribute :key, :string
57
+ attribute :value, :string
58
+ end
59
+
60
+ # app/models/user.rb
61
+ class User < ActiveRecord::Base
62
+ attribute :settings, UserSettings.to_type
63
+ attribute :preferences, Preference.to_array_type
64
+ end
65
+ ```
66
+
67
+ Running `bundle exec tapioca dsl` will generate the following RBI file:
68
+
69
+ ```ruby
70
+ # sorbet/rbi/dsl/user.rbi
71
+ # typed: strong
72
+
73
+ class User
74
+ sig { returns(T.nilable(UserSettings)) }
75
+ def settings; end
76
+
77
+ sig { params(value: T.nilable(T.any(UserSettings, T::Hash[T.untyped, T.untyped]))).returns(T.nilable(UserSettings)) }
78
+ def settings=(value); end
79
+
80
+ sig { params(attributes: T::Hash[T.untyped, T.untyped]).returns(UserSettings) }
81
+ def build_settings(attributes: {}); end
82
+
83
+ sig { returns(T::Array[Preference]) }
84
+ def preferences; end
85
+
86
+ sig { params(value: T.nilable(T.any(T::Array[Preference], T::Array[T::Hash[T.untyped, T.untyped]]))).returns(T::Array[Preference]) }
87
+ def preferences=(value); end
88
+ end
89
+ ```
90
+
91
+ ### Supported StoreModel Types
92
+
93
+ This compiler supports all StoreModel attribute types:
94
+
95
+ - **Single Models**: `Model.to_type` - generates getter, setter, and builder methods
96
+ - **Array Models**: `Model.to_array_type` - generates getter and setter methods for arrays
97
+ - **Nested Models**: StoreModel classes that contain other StoreModel attributes
98
+
99
+ ### Generated Methods
100
+
101
+ For each StoreModel attribute, the compiler generates type signatures for:
102
+
103
+ 1. **Getter method**: Returns the StoreModel instance or array
104
+ 2. **Setter method**: Accepts StoreModel instance(s) or Hash(es)
105
+ 3. **Builder method** (single types only): Creates a new instance with given attributes
106
+
107
+ ## Limitations
108
+
109
+ Currently, this compiler has the following limitations:
110
+
111
+ - **Enum Support**: Does not generate RBI signatures for enum methods (e.g., predicate methods like `active?`, bang methods like `status_active!`)
112
+ - **Nested Attributes**: Does not support `accepts_nested_attributes_for` generated methods
113
+ - **Custom Types**: Only supports `StoreModel::Types::One` and `StoreModel::Types::Many`, custom types are not detected
114
+ - **Validation Methods**: Does not generate signatures for StoreModel validation methods
115
+
116
+ These features may be added in future versions.
117
+
118
+ ## Requirements
119
+
120
+ - Ruby 2.7+
121
+ - [Tapioca](https://github.com/Shopify/tapioca) 0.10+
122
+ - [StoreModel](https://github.com/DmitryTsepelev/store_model) 1.0+
123
+ - ActiveRecord 6.0+
124
+
125
+ ## Development
126
+
127
+ After checking out the repo, run:
128
+
129
+ ```bash
130
+ $ bundle install
131
+ ```
132
+
133
+ To run the test suite:
134
+
135
+ ```bash
136
+ $ bundle exec rspec
137
+ ```
138
+
139
+ To run the linter:
140
+
141
+ ```bash
142
+ $ bundle exec rubocop
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Bug reports and pull requests are welcome on GitHub at https://github.com/speria-jp/tapioca_dsl_compiler_store_model.
148
+
149
+ ## License
150
+
151
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[rubocop spec]
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tapioca DSL Compiler for StoreModel
4
+ module Tapioca
5
+ module Dsl
6
+ module Compilers
7
+ class StoreModel < Tapioca::Dsl::Compiler
8
+ ConstantType = type_member { { fixed: T.class_of(ActiveRecord::Base) } }
9
+
10
+ sig { override.returns(T::Enumerable[Module]) }
11
+ def self.gather_constants
12
+ return [] unless defined?(::StoreModel)
13
+
14
+ ::ActiveRecord::Base.descendants.select do |klass|
15
+ next false unless klass.respond_to?(:attribute_types)
16
+
17
+ # Skip classes that can't load their schema or attributes
18
+ begin
19
+ attribute_types = klass.attribute_types
20
+ rescue ActiveRecord::TableNotSpecified, StandardError
21
+ next false
22
+ end
23
+
24
+ # Check if any attribute types are StoreModel types
25
+ attribute_types.values.any? { |type| store_model_type?(type) }
26
+ end
27
+ end
28
+
29
+ sig { override.void }
30
+ def decorate
31
+ root.create_path(constant) do |klass|
32
+ create_store_model_methods(klass)
33
+ end
34
+ end
35
+
36
+ sig { params(type: T.untyped).returns(T::Boolean) }
37
+ def self.store_model_type?(type)
38
+ return true if type.is_a?(::StoreModel::Types::One)
39
+ return true if type.is_a?(::StoreModel::Types::Many)
40
+ return true if store_model_class_type?(type)
41
+ return true if store_model_one_of_type?(type)
42
+
43
+ false
44
+ end
45
+
46
+ sig { params(type: T.untyped).returns(T::Boolean) }
47
+ def self.store_model_class_type?(type)
48
+ return false unless type.respond_to?(:model_klass)
49
+
50
+ model_klass = type.model_klass
51
+ return false if model_klass.nil?
52
+
53
+ model_klass.include?(::StoreModel::Model)
54
+ end
55
+
56
+ sig { params(type: T.untyped).returns(T::Boolean) }
57
+ def self.store_model_one_of_type?(type)
58
+ # Check for StoreModel.one_of types (polymorphic types)
59
+ return true if type.is_a?(::StoreModel::Types::OnePolymorphic)
60
+ return true if type.is_a?(::StoreModel::Types::ManyPolymorphic)
61
+
62
+ false
63
+ end
64
+
65
+ private
66
+
67
+ sig { params(mod: T.untyped).void }
68
+ def create_store_model_methods(mod)
69
+ constant.attribute_types.each do |attribute_name, type|
70
+ next unless self.class.store_model_type?(type)
71
+
72
+ create_attribute_methods(mod, attribute_name, type)
73
+ end
74
+ end
75
+
76
+ sig { params(mod: T.untyped, attribute_name: String, type: T.untyped).void }
77
+ def create_attribute_methods(mod, attribute_name, type)
78
+ case type
79
+ when ::StoreModel::Types::One
80
+ create_single_store_model_methods(mod, attribute_name, type.model_klass)
81
+ when ::StoreModel::Types::Many
82
+ create_many_store_model_methods(mod, attribute_name, type.model_klass)
83
+ else
84
+ if self.class.store_model_one_of_type?(type)
85
+ create_one_of_store_model_methods(mod, attribute_name, type)
86
+ else
87
+ create_fallback_store_model_methods(mod, attribute_name, type)
88
+ end
89
+ end
90
+ end
91
+
92
+ sig { params(mod: T.untyped, attribute_name: String, type: T.untyped).void }
93
+ def create_fallback_store_model_methods(mod, attribute_name, type)
94
+ return unless type.respond_to?(:model_klass)
95
+ return unless type.model_klass&.include?(::StoreModel::Model)
96
+
97
+ if array_type?(type)
98
+ create_many_store_model_methods(mod, attribute_name, type.model_klass)
99
+ else
100
+ create_single_store_model_methods(mod, attribute_name, type.model_klass)
101
+ end
102
+ end
103
+
104
+ sig { params(type: T.untyped).returns(T::Boolean) }
105
+ def array_type?(type)
106
+ type_name = type.class.name
107
+ type_name&.include?("Many") || type_name&.include?("Array")
108
+ end
109
+
110
+ sig { params(mod: T.untyped, attribute_name: String, type: T.untyped).void }
111
+ def create_one_of_store_model_methods(mod, attribute_name, type)
112
+ # OneOf types have dynamic model selection, so we use more generic types
113
+ if array_type?(type)
114
+ create_one_of_array_methods(mod, attribute_name)
115
+ else
116
+ create_one_of_single_methods(mod, attribute_name)
117
+ end
118
+ end
119
+
120
+ sig { params(mod: T.untyped, attribute_name: String).void }
121
+ def create_one_of_single_methods(mod, attribute_name)
122
+ # OneOf types are dynamically resolved, so we use generic StoreModel::Model types
123
+ mod.create_method(
124
+ attribute_name,
125
+ return_type: "T.nilable(StoreModel::Model)"
126
+ )
127
+
128
+ mod.create_method(
129
+ "#{attribute_name}=",
130
+ parameters: [create_param("value",
131
+ type: "T.nilable(T.any(StoreModel::Model, T::Hash[T.untyped, T.untyped]))")],
132
+ return_type: "T.nilable(StoreModel::Model)"
133
+ )
134
+
135
+ mod.create_method(
136
+ "build_#{attribute_name}",
137
+ parameters: [create_kw_opt_param("attributes", type: "T::Hash[T.untyped, T.untyped]", default: "{}")],
138
+ return_type: "StoreModel::Model"
139
+ )
140
+ end
141
+
142
+ sig { params(mod: T.untyped, attribute_name: String).void }
143
+ def create_one_of_array_methods(mod, attribute_name)
144
+ # OneOf array types are dynamically resolved
145
+ mod.create_method(
146
+ attribute_name,
147
+ return_type: "T::Array[StoreModel::Model]"
148
+ )
149
+
150
+ mod.create_method(
151
+ "#{attribute_name}=",
152
+ parameters: [create_param("value",
153
+ type: "T.nilable(T.any(T::Array[StoreModel::Model], " \
154
+ "T::Array[T::Hash[T.untyped, T.untyped]]))")],
155
+ return_type: "T::Array[StoreModel::Model]"
156
+ )
157
+ end
158
+
159
+ sig { params(mod: T.untyped, attribute_name: String, model_klass: T.untyped).void }
160
+ def create_single_store_model_methods(mod, attribute_name, model_klass)
161
+ return_type = model_klass.name
162
+
163
+ mod.create_method(
164
+ attribute_name,
165
+ return_type: "T.nilable(#{return_type})"
166
+ )
167
+
168
+ mod.create_method(
169
+ "#{attribute_name}=",
170
+ parameters: [create_param("value",
171
+ type: "T.nilable(T.any(#{return_type}, T::Hash[T.untyped, T.untyped]))")],
172
+ return_type: "T.nilable(#{return_type})"
173
+ )
174
+
175
+ mod.create_method(
176
+ "build_#{attribute_name}",
177
+ parameters: [create_kw_opt_param("attributes", type: "T::Hash[T.untyped, T.untyped]", default: "{}")],
178
+ return_type: return_type
179
+ )
180
+ end
181
+
182
+ sig { params(mod: T.untyped, attribute_name: String, model_klass: T.untyped).void }
183
+ def create_many_store_model_methods(mod, attribute_name, model_klass)
184
+ return_type = model_klass.name
185
+ array_type = "T::Array[#{return_type}]"
186
+
187
+ mod.create_method(
188
+ attribute_name,
189
+ return_type: array_type
190
+ )
191
+
192
+ mod.create_method(
193
+ "#{attribute_name}=",
194
+ parameters: [create_param("value",
195
+ type: "T.nilable(T.any(#{array_type}, " \
196
+ "T::Array[T::Hash[T.untyped, T.untyped]]))")],
197
+ return_type: array_type
198
+ )
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TapiocaDslCompilerStoreModel
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tapioca_dsl_compiler_store_model/version"
4
+
5
+ # Conditionally load required dependencies
6
+ begin
7
+ require "tapioca/dsl"
8
+ require "store_model"
9
+ rescue LoadError
10
+ # Do nothing if dependencies are not available
11
+ end
12
+
13
+ # Only load implementation when both Tapioca::Dsl::Compiler and StoreModel are available
14
+ require_relative "tapioca/dsl/compilers/store_model" if defined?(Tapioca::Dsl::Compiler) && defined?(StoreModel)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tapioca_dsl_compiler_store_model/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tapioca_dsl_compiler_store_model"
7
+ spec.version = TapiocaDslCompilerStoreModel::VERSION
8
+ spec.authors = ["speria-jp"]
9
+
10
+ spec.summary = "Tapioca DSL compiler for StoreModel"
11
+ spec.description = "Provides Tapioca DSL compiler for generating RBI files for StoreModel gem"
12
+ spec.homepage = "https://github.com/speria-jp/tapioca_dsl_compiler_store_model"
13
+ spec.email = ["daichi.sakai@speria.jp"]
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["rubygems_mfa_required"] = "true"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (File.expand_path(f) == __FILE__) ||
24
+ f.start_with?("test/", "spec/", "features/") ||
25
+ f.start_with?(".github/") ||
26
+ f == ".gitignore" ||
27
+ f == ".rspec" ||
28
+ f == ".rubocop.yml" ||
29
+ f == "Gemfile" ||
30
+ f == "Gemfile.lock" ||
31
+ f == "CLAUDE.md"
32
+ end
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_dependency "store_model", ">= 1.0.0"
39
+ spec.add_dependency "tapioca", ">= 0.11.0"
40
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tapioca_dsl_compiler_store_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - speria-jp
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: store_model
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: tapioca
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.11.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.11.0
40
+ description: Provides Tapioca DSL compiler for generating RBI files for StoreModel
41
+ gem
42
+ email:
43
+ - daichi.sakai@speria.jp
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - Rakefile
50
+ - lib/tapioca/dsl/compilers/store_model.rb
51
+ - lib/tapioca_dsl_compiler_store_model.rb
52
+ - lib/tapioca_dsl_compiler_store_model/version.rb
53
+ - tapioca_dsl_compiler_store_model.gemspec
54
+ homepage: https://github.com/speria-jp/tapioca_dsl_compiler_store_model
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/speria-jp/tapioca_dsl_compiler_store_model
59
+ source_code_uri: https://github.com/speria-jp/tapioca_dsl_compiler_store_model
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.2.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.7
76
+ specification_version: 4
77
+ summary: Tapioca DSL compiler for StoreModel
78
+ test_files: []