stretchy-model 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a85f7bffcc11c54366d4cae7fec47b91341425e06b7f1c8a6269e0b23d4e649b
4
- data.tar.gz: cd0b9314fa734aa7af7179f12ed00626a17bc869186f82088158bfca87c8d915
3
+ metadata.gz: dd18b424c18bda352233d72af113e9bbc944277e03a02cfb950780f0fab9a405
4
+ data.tar.gz: 6eb86522e7bc91012cc4c743d0a9fccc54c7e75b14ff4d6f4fca5bdb9fa31626
5
5
  SHA512:
6
- metadata.gz: e193c837695100177eac9888841b14243596dfc9694515696ab1a9934174287c821cf52b2ca165e3f3eda04de9ba16c7c7330cb187641d71229d322c06674977
7
- data.tar.gz: 21ace7b64f85d505a05b61b89b5b85d9d2af34941b57672da45f70e6c8736e8b9451c59891fef7d0795f8b54076cffd88bfc141fcd4da1c1f8ffaddcf8276697
6
+ metadata.gz: 0fd2a0fb25439d79c799f43f43bb01df7c4dfe0ce9eb577d445b6d415eb9c38ecdc0f45c73d8255d2e079ff502e943f6491b874e3bb9c41fc9c853efed84d252
7
+ data.tar.gz: 8ea4cfa584ec825ac30aa4aa5dea88b04c709cbf7cc75089e28cd1e850318f150f1f68ead89bc571535739aa8ff8db777102d5dd05173087d4af499bac8d8bbb
data/.rspec CHANGED
@@ -1 +1 @@
1
- --require spec_helper
1
+ --require spec_helper
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  stretchy-model
2
2
  ===
3
-
4
3
  <p>
5
4
  <a href="https://stretchy.io/" target="_blank"><img src="./stretchy.logo.png" alt="Gum Image" width="450" /></a>
6
5
  <br><br>
@@ -9,114 +8,42 @@ stretchy-model
9
8
 
10
9
  </p>
11
10
 
12
- Stretchy provides Elasticsearch models in a Rails environment with an integrated ActiveRecord-like interface and features.
13
11
 
14
12
  ## Features
15
13
  Stretchy simplifies the process of querying, aggregating, and managing Elasticsearch-backed models, allowing Rails developers to work with search indices as comfortably as they would with traditional Rails models.
16
14
 
17
- ## Attributes
18
-
19
- ```ruby
20
- class Post < Stretchy::Record
21
-
22
- attribute :title, :string
23
- attribute :body, :string
24
- attribute :flagged, :boolean, default: false
25
- attribute :author, :hash
26
- attribute :tags, :array, default: []
27
-
28
- end
29
- ```
30
- >[!NOTE]
31
- >`created_at`, `:updated_at` and `:id` are automatically added to all `Stretchy::Records`
32
-
33
-
34
- ## Query
35
- ```ruby
36
- Post.where('author.name': "Jadzia", flagged: true).first
37
- #=> <Post id: aW02w3092, title: "Fun Cats", body: "...", flagged: true,
38
- # author: {name: "Jadzia", age: 20}, tags: ["cat", "amusing"]>
39
- ```
40
-
41
- ## Aggregations
42
- ```ruby
43
-
44
- result = Post.filter(:range, 'author.age': {gte: 18})
45
- .aggregation(:post_frequency, date_histogram: {
46
- field: :created_at,
47
- calender_interval: :month
48
- })
15
+ * Model fully back by Elasticsearch/Opensearch
16
+ * Chain queries, scopes and aggregations
17
+ * Reduce Elasticsearch query complexity
18
+ * Support for time-based indices and aliases
19
+ * Associations to both ActiveRecord models and Stretchy::Record
20
+ * Bulk Operations made easy
21
+ * Validations, custom attributes, and more...
49
22
 
50
- result.aggregations.post_frequency
51
- #=> {buckets: [{key_as_string: "2024-01-01", doc_count: 20}, ...]}
52
- ```
53
-
54
- ## Scopes
55
-
56
- ```ruby
57
- class Post < Stretchy::Record
58
- # ...attributes
59
-
60
- # Scopes
61
- scope :flagged, -> { where(flagged: true) }
62
- scope :top_links, lambda do |size=10, url=".com"|
63
- aggregation(:links,
64
- terms: {
65
- field: :links,
66
- size: size,
67
- include: ".*#{url}.*"
68
- })
69
- end
70
- end
71
-
72
- # Returns flagged posts and includes the top 10 'youtube.com'
73
- # links in results.aggregations.links
74
- result = Post.flagged.top_links(10, "youtube.com")
75
-
76
- ```
23
+ Follow the guides to learn more about:
77
24
 
78
- ## Bulk Operations
79
-
80
-
81
- ```ruby
82
- Model.bulk(records_as_bulk_operations)
83
- ```
84
-
85
- #### Bulk helper
86
- Generates structure for the bulk operation
87
- ```ruby
88
- record.to_bulk # default to_bulk(:index)
89
- record.to_bulk(:delete)
90
- record.to_bulk(:update)
91
- ```
25
+ * [Models](https://theablefew.github.io/stretchy/#/guides/models?id=models)
26
+ * [Querying](https://theablefew.github.io/stretchy/#/guides/querying?id=querying)
27
+ * [Aggregations](https://theablefew.github.io/stretchy/#/guides/aggregations?id=aggregations)
28
+ * [Scopes](https://theablefew.github.io/stretchy/#/guides/scopes?id=scopes)
92
29
 
93
- #### In batches
94
- Run bulk operations in batches specified by `size`
95
- ```ruby
96
- Model.bulk_in_batches(records, size: 100) do |batch|
97
- batch.map! { |record| Model.new(record).to_bulk }
98
- end
99
- ```
100
30
 
31
+ [Read the Documentation](https://theablefew.github.io/stretchy/#/) or walk through of a simple [Data Analysis](https://theablefew.github.io/stretchy/#/examples/data_analysis?id=data-analysis) example.
101
32
 
102
- ## Instrumentation
103
- ```ruby
104
- Blanket.first
105
- ```
106
33
 
107
- ```sh
108
- Blanket (6.322ms) curl -X GET 'http://localhost:9200/blankets/_search?size=1' -d '{"sort":{"date":"desc"}}'
109
- ```
110
34
 
111
35
  ## Installation
112
36
 
113
37
  Install the gem and add to the application's Gemfile by executing:
114
38
 
115
- $ bundle add stretchy-model
39
+ ```sh
40
+ bundle add stretchy-model
41
+ ```
116
42
 
117
43
  If bundler is not being used to manage dependencies, install the gem by executing:
118
-
119
- $ gem install stretchy-model
44
+ ```sh
45
+ gem install stretchy-model
46
+ ```
120
47
 
121
48
  <details>
122
49
  <summary>Rails Configuration</summary>
@@ -131,6 +58,12 @@ rails credentials:edit
131
58
  ```yaml
132
59
  elasticsearch:
133
60
  url: localhost:9200
61
+
62
+ # or opensearch
63
+ # opensearch:
64
+ # host: https://localhost:9200
65
+ # user: admin
66
+ # password: admin
134
67
  ```
135
68
 
136
69
  #### Create an initializer
@@ -149,11 +82,24 @@ end
149
82
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
150
83
 
151
84
  >[!TIP]
152
- >This library is built on top of the excellent [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence) gem.
153
- >
154
85
  > Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence)
155
86
 
156
87
  ## Testing
88
+ <details>
89
+ <summary>Act</summary>
90
+
91
+ Run github action workflow locally
92
+
93
+ ```sh
94
+ brew install act --HEAD
95
+ ```
96
+
97
+ ```sh
98
+ act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest
99
+ ```
100
+
101
+ </details>
102
+
157
103
  <details>
158
104
  <summary>Elasticsearch</summary>
159
105
 
data/Rakefile CHANGED
@@ -2,3 +2,95 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  task default: %i[]
5
+
6
+ require 'octokit'
7
+ require 'versionomy'
8
+ require 'rainbow'
9
+
10
+ def determine_current_version
11
+ # Load current version
12
+ load 'lib/stretchy/version.rb'
13
+ current_version = Versionomy.parse(Stretchy::VERSION)
14
+ end
15
+
16
+ def determine_new_version(version)
17
+ # Load current version
18
+ current_version = determine_current_version
19
+
20
+ # Determine new version
21
+ case version.to_sym
22
+ when :major
23
+ current_version.bump(:major)
24
+ when :minor
25
+ current_version.bump(:minor)
26
+ when :patch
27
+ current_version.bump(:tiny)
28
+ else
29
+ version =~ /\Av?\d+\.\d+\.\d+\z/ ? Versionomy.parse(version).to_s.gsub(/v/,'') : current_version
30
+ end
31
+ end
32
+
33
+ def create_release_branch(new_version, base_branch)
34
+ system("git stash save 'Changes before creating release branch'")
35
+ system("git fetch origin #{base_branch}")
36
+ branch_name = "release/v#{new_version}"
37
+ system("git checkout -b #{branch_name} #{base_branch}")
38
+ branch_name
39
+ end
40
+
41
+ def update_version_file(new_version)
42
+ # Update lib/stretchy/version.rb
43
+ File.open('lib/stretchy/version.rb', 'w') do |file|
44
+ file.puts "module Stretchy\n VERSION = '#{new_version}'\nend"
45
+ end
46
+ end
47
+
48
+ def commit_and_push_changes(new_version, branch_name)
49
+ system("git add lib/stretchy/version.rb")
50
+ system("git commit -m 'Bump version to v#{new_version}'")
51
+ system("git tag v#{new_version}")
52
+ system("git push origin #{branch_name} --tags -f")
53
+ end
54
+
55
+ def create_pull_request(new_version, base_branch, branch_name)
56
+ # Create a pull request
57
+ client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])
58
+ client.create_pull_request('theablefew/stretchy', base_branch, branch_name, "Release v#{new_version}")
59
+ end
60
+
61
+ namespace :publish do
62
+ desc "Create a release"
63
+ task :release, [:version, :base_branch] do |t, args|
64
+ args.with_defaults(version: :patch, base_branch: 'main')
65
+ version = args[:version]
66
+ base_branch = args[:base_branch]
67
+
68
+ old_version = determine_current_version
69
+ new_version = determine_new_version(version)
70
+ puts Rainbow("Bumping version from #{old_version} to #{new_version}").green
71
+ branch_name = create_release_branch(new_version, base_branch)
72
+ begin
73
+ update_version_file(new_version)
74
+ commit_and_push_changes(new_version, branch_name)
75
+ create_pull_request(new_version, base_branch, branch_name)
76
+ rescue => e
77
+ puts "Error: #{e.message}"
78
+ puts "Rolling back changes"
79
+ system("git tag -d v#{new_version}")
80
+ system("git checkout #{base_branch}")
81
+ system("git branch -D #{branch_name}")
82
+ end
83
+ end
84
+
85
+ task :major do
86
+ Rake::Task['publish:release'].invoke('major')
87
+ end
88
+
89
+ task :minor do
90
+ Rake::Task['publish:release'].invoke('minor')
91
+ end
92
+
93
+ task :patch do
94
+ Rake::Task['publish:release'].invoke('patch')
95
+ end
96
+ end
@@ -3,7 +3,11 @@ module Stretchy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  def save!
6
+ if valid?
6
7
  self.save
8
+ else
9
+ raise "Record is invalid"
10
+ end
7
11
  end
8
12
 
9
13
  # Required for Elasticsearch < 7
@@ -34,7 +38,7 @@ module Stretchy
34
38
  end
35
39
 
36
40
  def association_reflection(association)
37
- ElasticRelation.new @@_associations[association], (dirty[association.to_sym] || [])
41
+ Stretchy::Relation.new @@_associations[association], (dirty[association.to_sym] || [])
38
42
  end
39
43
 
40
44
  def _destroy=(bool)
@@ -48,6 +52,7 @@ module Stretchy
48
52
  def save_associations
49
53
  @_after_save_objects.each_pair do |association, collection|
50
54
  collection.each do |instance|
55
+ # TODO: bulk update
51
56
  instance.send("#{@@_association_options[association.to_sym][:foreign_key]}=", self.id)
52
57
  instance.save
53
58
  end
@@ -59,59 +64,194 @@ module Stretchy
59
64
  @@_associations ||= {}
60
65
  @@_association_options ||= {}
61
66
 
67
+ # The belongs_to method is used to set up a one-to-one connection with another model.
68
+ # This indicates that this model has exactly one instance of another model.
69
+ # For example, if your application includes authors and books, and each book can be assigned exactly one author,
70
+ # you'd declare the book model to belong to the author model.
71
+ #
72
+ # association:: [Symbol] the name of the association
73
+ # options:: [Hash] a hash to set up options for the association
74
+ # :foreign_key - the foreign key used for the association. Defaults to "#{association}_id"
75
+ # :primary_key - the primary key used for the association. Defaults to "id"
76
+ # :class_name - the name of the associated object's class. Defaults to the name of the association
77
+ #
78
+ # Example:
79
+ # belongs_to :author
80
+ #
81
+ # This creates a book.author method that returns the author of the book.
82
+ # It also creates an author= method that allows you to assign the author of the book.
83
+ #
62
84
  def belongs_to(association, options = {})
63
85
  @@_association_options[association] = {
64
86
  foreign_key: "#{association}_id",
65
87
  primary_key: "id",
66
88
  class_name: association
67
- }.reverse_merge(options)
89
+ }.merge(options)
68
90
 
69
91
  klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
70
92
  @@_associations[association] = klass
71
93
 
72
94
  define_method(association.to_sym) do
73
- klass.where(_id: self.send(@@_association_options[association][:foreign_key].to_sym)).first
95
+ instance_variable_get("@#{association}") ||
96
+ klass.where(_id: self.send(@@_association_options[association][:foreign_key].to_sym)).first
74
97
  end
75
98
 
76
99
  define_method("#{association}=".to_sym) do |val|
77
100
  options = @@_association_options[association]
78
- instance_variable_set("@#{options[:foreign_key]}", val.send(options[:primary_key]))
101
+ self.send("#{options[:foreign_key]}=", val.send(options[:primary_key]))
102
+ instance_variable_set("@#{association}", val)
103
+ end
104
+
105
+ define_method("build_#{association}") do |*args|
106
+ associated_object = klass.new(*args)
107
+ instance_variable_set("@#{association}", associated_object)
108
+ associated_object
109
+ end
110
+
111
+ before_save do
112
+ associated_object = instance_variable_get("@#{association}")
113
+ if associated_object && associated_object.new_record?
114
+ if associated_object.save!
115
+ self.send("#{@@_association_options[association][:foreign_key]}=", associated_object.id)
116
+ end
117
+ end
79
118
  end
80
119
  end
81
120
 
82
- def has_one(association, class_name: nil, foreign_key: nil, dependent: :destroy)
83
121
 
84
- klass = association.to_s.singularize.classify.constantize unless class_name.present?
85
- foreign_key = "#{self.name.downcase}_id" unless foreign_key.present?
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+ # The has_one method is used to set up a one-to-one connection with another model.
131
+ # This indicates that this model contains the foreign key.
132
+ #
133
+ # association:: [Symbol] The name of the association.
134
+ # options:: [Hash] A hash to set up options for the association.
135
+ # :class_name - The name of the associated model. If not provided, it's derived from +association+.
136
+ # :foreign_key - The name of the foreign key on the associated model. If not provided, it's derived from the name of this model.
137
+ # :dependent - If set to +:destroy+, the associated object will be destroyed when this object is destroyed. This is the default behavior.
138
+ # :primary_key - The name of the primary key on the associated model. If not provided, it's assumed to be +id+.
139
+ #
140
+ #
141
+ # Example:
142
+ # has_one :profile
143
+ #
144
+ # This creates a user.profile method that returns the profile of the user.
145
+ # It also creates a profile= method that allows you to assign the profile of the user.
146
+ #
147
+ def has_one(association, options = {})
148
+
149
+ @@_association_options[association] = {
150
+ foreign_key: "#{self.name.underscore}_id",
151
+ primary_key: "id",
152
+ class_name: association
153
+ }.merge(options)
154
+
155
+ klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
86
156
  @@_associations[association] = klass
87
157
 
158
+ foreign_key = @@_association_options[association][:foreign_key]
159
+
88
160
  define_method(association.to_sym) do
89
- klass.where("#{foreign_key}": self.id).first
161
+ instance_variable_get("@#{association}") ||
162
+ klass.where("#{foreign_key}": self.id).first
163
+ end
164
+
165
+ define_method("#{association}=".to_sym) do |val|
166
+ instance_variable_set("@#{association}", val)
167
+ save!
168
+ end
169
+
170
+ before_save do
171
+ associated_object = instance_variable_get("@#{association}")
172
+ if associated_object
173
+ associated_object.send("#{foreign_key}=", self.id)
174
+ associated_object.save!
175
+ end
90
176
  end
91
177
  end
92
178
 
93
- def has_many(association, klass, options = {})
94
- @@_associations[association] = klass
95
179
 
96
- opt_fk = options.delete(:foreign_key)
97
- foreign_key = opt_fk ? opt_fk : "#{self.name.split("::").last.tableize.singularize}_id"
98
180
 
99
- @@_association_options[association] = { foreign_key: foreign_key }
181
+
182
+
183
+
184
+
185
+
186
+
187
+ # The has_many method is used to set up a one-to-many connection with another model.
188
+ # This indicates that this model can be matched with zero or more instances of another model.
189
+ # For example, if your application includes authors and books, and each author can have many books,
190
+ # you'd declare the author model to have many books.
191
+ #
192
+ # association:: [Symbol] the name of the association
193
+ # options:: [Hash] a hash to set up options for the association
194
+ # :foreign_key - the foreign key used for the association. Defaults to "#{self.name.downcase}_id"
195
+ # :primary_key - the primary key used for the association. Defaults to "id"
196
+ # :class_name - the name of the associated object's class. Defaults to the name of the association
197
+ # :dependent - if set to :destroy, the associated object will be destroyed when this object is destroyed. This is the default behavior.
198
+ #
199
+ #
200
+ # Example:
201
+ # has_many :books
202
+ #
203
+ # This creates an author.books method that returns a collection of books for the author.
204
+ # It also creates a books= method that allows you to assign the books for the author.
205
+ #
206
+ def has_many(association, options = {})
207
+ @@_association_options[association] = {
208
+ foreign_key: "#{self.name.underscore}_id",
209
+ primary_key: "id",
210
+ class_name: association.to_s.singularize.to_sym
211
+ }.merge(options)
212
+
213
+ klass = @@_association_options[association][:class_name].to_s.classify.constantize
214
+ foreign_key = @@_association_options[association][:foreign_key]
215
+ primary_key = @@_association_options[association][:primary_key]
216
+ @@_associations[association] = klass
100
217
 
101
218
  define_method(association.to_sym) do
102
219
  args = {}
103
- args[foreign_key] = self.id
220
+ args["_#{primary_key}"] = self.send("#{association.to_s.singularize}_ids")
104
221
  self.new_record? ? association_reflection(association) : klass.where(args)
105
222
  end
106
223
 
224
+ define_method("#{association.to_s.singularize}_ids") do
225
+ instance_variable_get("@#{association.to_s.singularize}_ids".to_sym)
226
+ end
227
+
228
+ define_method("#{association.to_s.singularize}_ids=") do |val|
229
+ instance_variable_set("@#{association.to_s.singularize}_ids".to_sym, val)
230
+ end
231
+
232
+ define_method("#{association}=".to_sym) do |val|
233
+ val.each { |v| after_save_objects(v.attributes, association)}
234
+ self.send("#{association.to_s.singularize}_ids=", val.map(&:id))
235
+ dirty
236
+ end
237
+
107
238
  define_method("build_#{association}".to_sym) do |*args|
108
239
  opts = {}
109
240
  opts[foreign_key] = self.id
110
241
  args.first.merge! opts
111
242
  klass.new *args
112
243
  end
244
+
245
+ after_save do
246
+ save_associations
247
+ end
113
248
  end
114
249
 
250
+
251
+
252
+
253
+
254
+
115
255
  def validates_associated(*attr_names)
116
256
  validates_with AssociatedValidator, _merge_attributes(attr_names)
117
257
  end
@@ -131,7 +271,7 @@ module Stretchy
131
271
  end
132
272
 
133
273
  def reflect_on_association(association)
134
- ElasticRelation.new @@_associations[association]
274
+ Stretchy::Relation.new @@_associations[association]
135
275
  end
136
276
 
137
277
  def update_all(records, **attributes)
@@ -0,0 +1,85 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Transformers
4
+ class KeywordTransformer
5
+
6
+ KEYWORD_AGGREGATION_KEYS = [:terms, :rare_terms, :significant_terms, :cardinality, :string_stats]
7
+
8
+ attr_reader :attribute_types
9
+
10
+ def initialize(attribute_types)
11
+ @attribute_types = attribute_types
12
+ end
13
+
14
+ def cast_value_keys
15
+ values.transform_values do |value|
16
+ case value
17
+ when Array
18
+ value.map { |item| transform_keys_for_item(item) }
19
+ when Hash
20
+ transform_keys_for_item(value)
21
+ else
22
+ value
23
+ end
24
+ end
25
+ end
26
+
27
+ def keyword?(arg)
28
+ attr = @attribute_types[arg.to_s]
29
+ return false unless attr
30
+ attr.is_a?(Stretchy::Attributes::Type::Keyword)
31
+ end
32
+
33
+ def protected?(arg)
34
+ return false if arg.nil?
35
+ Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS.include?(arg.to_sym)
36
+ end
37
+
38
+ def transform(item, *ignore)
39
+ item.each_with_object({}) do |(k, v), new_item|
40
+ if ignore && ignore.include?(k)
41
+ new_item[k] = v
42
+ next
43
+ end
44
+ new_key = (!protected?(k) && keyword?(k)) ? "#{k}.keyword" : k
45
+
46
+ new_value = v
47
+
48
+ if new_value.is_a?(Hash)
49
+ new_value = transform(new_value)
50
+ elsif new_value.is_a?(Array)
51
+ new_value = new_value.map { |i| i.is_a?(Hash) ? transform(i) : i }
52
+ elsif new_value.is_a?(String) || new_value.is_a?(Symbol)
53
+ new_value = "#{new_value}.keyword" if keyword?(new_value)
54
+ end
55
+
56
+ new_item[new_key] = new_value
57
+ end
58
+ end
59
+
60
+ # If terms are used, we assume that the field is a keyword field
61
+ # and append .keyword to the field name
62
+ # {terms: {field: 'gender'}}
63
+ # or nested aggs
64
+ # {terms: {field: 'gender'}, aggs: {name: {terms: {field: 'position.name'}}}}
65
+ # should be converted to
66
+ # {terms: {field: 'gender.keyword'}, aggs: {name: {terms: {field: 'position.name.keyword'}}}}
67
+ # {date_histogram: {field: 'created_at', interval: 'day'}}
68
+ # TODO: There may be cases where we don't want to add .keyword to the field and there should be a way to override this
69
+ def assume_keyword_field(args={}, parent_match=false)
70
+ if args.is_a?(Hash)
71
+ args.each do |k, v|
72
+ if v.is_a?(Hash)
73
+ assume_keyword_field(v, KEYWORD_AGGREGATION_FIELDS.include?(k))
74
+ else
75
+ next unless v.is_a?(String) || v.is_a?(Symbol)
76
+ args[k] = ([:field, :fields].include?(k.to_sym) && v !~ /\.keyword$/ && parent_match) ? "#{v}.keyword" : v.to_s
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,15 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+
5
+ class Array < ActiveModel::Type::Value # :nodoc:
6
+
7
+ def type
8
+ :array
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ class Hash < ActiveModel::Type::Value # :nodoc:
5
+ def type
6
+ :hash
7
+ end
8
+
9
+ private
10
+
11
+ def cast_value(value)
12
+ Elasticsearch::Model::HashWrapper[value]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ class Keyword < ActiveModel::Type::String # :nodoc:
5
+ def type
6
+ :keyword
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ # alias for ActiveModel::Type::String
5
+ class Text < ActiveModel::Type::String
6
+ def type
7
+ :text
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end