superstore 1.0.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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGELOG +0 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE +13 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +100 -0
  9. data/Rakefile +12 -0
  10. data/lib/superstore/adapters/abstract_adapter.rb +49 -0
  11. data/lib/superstore/adapters/cassandra_adapter.rb +181 -0
  12. data/lib/superstore/adapters/hstore_adapter.rb +163 -0
  13. data/lib/superstore/attribute_methods/definition.rb +22 -0
  14. data/lib/superstore/attribute_methods/dirty.rb +36 -0
  15. data/lib/superstore/attribute_methods/primary_key.rb +25 -0
  16. data/lib/superstore/attribute_methods/typecasting.rb +59 -0
  17. data/lib/superstore/attribute_methods.rb +96 -0
  18. data/lib/superstore/base.rb +33 -0
  19. data/lib/superstore/belongs_to/association.rb +48 -0
  20. data/lib/superstore/belongs_to/builder.rb +40 -0
  21. data/lib/superstore/belongs_to/reflection.rb +30 -0
  22. data/lib/superstore/belongs_to.rb +63 -0
  23. data/lib/superstore/callbacks.rb +29 -0
  24. data/lib/superstore/cassandra_schema/statements.rb +52 -0
  25. data/lib/superstore/cassandra_schema/tasks.rb +47 -0
  26. data/lib/superstore/cassandra_schema.rb +9 -0
  27. data/lib/superstore/connection.rb +39 -0
  28. data/lib/superstore/core.rb +59 -0
  29. data/lib/superstore/errors.rb +10 -0
  30. data/lib/superstore/identity.rb +26 -0
  31. data/lib/superstore/inspect.rb +25 -0
  32. data/lib/superstore/log_subscriber.rb +44 -0
  33. data/lib/superstore/model.rb +37 -0
  34. data/lib/superstore/persistence.rb +153 -0
  35. data/lib/superstore/railtie.rb +30 -0
  36. data/lib/superstore/railties/controller_runtime.rb +45 -0
  37. data/lib/superstore/schema.rb +20 -0
  38. data/lib/superstore/scope/batches.rb +32 -0
  39. data/lib/superstore/scope/finder_methods.rb +48 -0
  40. data/lib/superstore/scope/query_methods.rb +49 -0
  41. data/lib/superstore/scope.rb +49 -0
  42. data/lib/superstore/scoping.rb +27 -0
  43. data/lib/superstore/tasks/ks.rake +54 -0
  44. data/lib/superstore/timestamps.rb +19 -0
  45. data/lib/superstore/type.rb +16 -0
  46. data/lib/superstore/types/array_type.rb +20 -0
  47. data/lib/superstore/types/base_type.rb +26 -0
  48. data/lib/superstore/types/boolean_type.rb +20 -0
  49. data/lib/superstore/types/date_type.rb +22 -0
  50. data/lib/superstore/types/float_type.rb +16 -0
  51. data/lib/superstore/types/integer_type.rb +20 -0
  52. data/lib/superstore/types/json_type.rb +13 -0
  53. data/lib/superstore/types/string_type.rb +19 -0
  54. data/lib/superstore/types/time_type.rb +16 -0
  55. data/lib/superstore/types.rb +8 -0
  56. data/lib/superstore/validations.rb +44 -0
  57. data/lib/superstore.rb +69 -0
  58. data/superstore.gemspec +23 -0
  59. data/test/support/cassandra.rb +44 -0
  60. data/test/support/hstore.rb +40 -0
  61. data/test/support/issue.rb +10 -0
  62. data/test/test_helper.rb +42 -0
  63. data/test/unit/active_model_test.rb +18 -0
  64. data/test/unit/adapters/adapter_test.rb +6 -0
  65. data/test/unit/attribute_methods/definition_test.rb +13 -0
  66. data/test/unit/attribute_methods/dirty_test.rb +72 -0
  67. data/test/unit/attribute_methods/primary_key_test.rb +26 -0
  68. data/test/unit/attribute_methods/typecasting_test.rb +118 -0
  69. data/test/unit/attribute_methods_test.rb +51 -0
  70. data/test/unit/base_test.rb +20 -0
  71. data/test/unit/belongs_to/reflection_test.rb +12 -0
  72. data/test/unit/belongs_to_test.rb +62 -0
  73. data/test/unit/callbacks_test.rb +46 -0
  74. data/test/unit/cassandra_schema/statements_test.rb +47 -0
  75. data/test/unit/cassandra_schema/tasks_test.rb +31 -0
  76. data/test/unit/connection_test.rb +10 -0
  77. data/test/unit/core_test.rb +55 -0
  78. data/test/unit/identity_test.rb +26 -0
  79. data/test/unit/inspect_test.rb +26 -0
  80. data/test/unit/log_subscriber_test.rb +26 -0
  81. data/test/unit/persistence_test.rb +213 -0
  82. data/test/unit/railties/controller_runtime_test.rb +48 -0
  83. data/test/unit/schema_test.rb +27 -0
  84. data/test/unit/scope/batches_test.rb +30 -0
  85. data/test/unit/scope/finder_methods_test.rb +51 -0
  86. data/test/unit/scope/query_methods_test.rb +27 -0
  87. data/test/unit/scoping_test.rb +7 -0
  88. data/test/unit/serialization_test.rb +10 -0
  89. data/test/unit/timestamps_test.rb +27 -0
  90. data/test/unit/types/array_type_test.rb +21 -0
  91. data/test/unit/types/base_type_test.rb +19 -0
  92. data/test/unit/types/boolean_type_test.rb +24 -0
  93. data/test/unit/types/date_type_test.rb +15 -0
  94. data/test/unit/types/float_type_test.rb +17 -0
  95. data/test/unit/types/integer_type_test.rb +19 -0
  96. data/test/unit/types/json_type_test.rb +23 -0
  97. data/test/unit/types/string_type_test.rb +30 -0
  98. data/test/unit/types/time_type_test.rb +19 -0
  99. data/test/unit/validations_test.rb +27 -0
  100. metadata +170 -0
@@ -0,0 +1,44 @@
1
+ module Superstore
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def self.runtime=(value)
4
+ Thread.current["cassandra_object_request_runtime"] = value
5
+ end
6
+
7
+ def self.runtime
8
+ Thread.current["cassandra_object_request_runtime"] ||= 0
9
+ end
10
+
11
+ def self.reset_runtime
12
+ rt, self.runtime = runtime, 0
13
+ rt
14
+ end
15
+
16
+ def initialize
17
+ super
18
+ @odd_or_even = false
19
+ end
20
+
21
+ def cql(event)
22
+ self.class.runtime += event.duration
23
+
24
+ payload = event.payload
25
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
26
+ cql = payload[:cql].squeeze(' ')
27
+
28
+ if odd?
29
+ name = color(name, CYAN, true)
30
+ cql = color(cql, nil, true)
31
+ else
32
+ name = color(name, MAGENTA, true)
33
+ end
34
+
35
+ debug " #{name} #{cql}"
36
+ end
37
+
38
+ def odd?
39
+ @odd_or_even = !@odd_or_even
40
+ end
41
+ end
42
+ end
43
+
44
+ Superstore::LogSubscriber.attach_to :cassandra_object
@@ -0,0 +1,37 @@
1
+ module Superstore
2
+ module Model
3
+ def column_family=(column_family)
4
+ @column_family = column_family
5
+ end
6
+
7
+ def column_family
8
+ @column_family ||= base_class.name.pluralize
9
+ end
10
+
11
+ def base_class
12
+ class_of_active_record_descendant(self)
13
+ end
14
+
15
+ def config=(config)
16
+ @@config = config.deep_symbolize_keys
17
+ end
18
+
19
+ def config
20
+ @@config
21
+ end
22
+
23
+ private
24
+
25
+ # Returns the class descending directly from ActiveRecord::Base or an
26
+ # abstract class, if any, in the inheritance hierarchy.
27
+ def class_of_active_record_descendant(klass)
28
+ if klass == Base || klass.superclass == Base
29
+ klass
30
+ elsif klass.superclass.nil?
31
+ raise "#{name} doesn't belong in a hierarchy descending from Superstore"
32
+ else
33
+ class_of_active_record_descendant(klass.superclass)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,153 @@
1
+ module Superstore
2
+ module Persistence
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :batch_statements
7
+ end
8
+
9
+ module ClassMethods
10
+ def remove(ids)
11
+ adapter.delete column_family, ids
12
+ end
13
+
14
+ def delete_all
15
+ adapter.execute "TRUNCATE #{column_family}"
16
+ end
17
+
18
+ def create(attributes = {}, &block)
19
+ new(attributes, &block).tap do |object|
20
+ object.save
21
+ end
22
+ end
23
+
24
+ def insert_record(id, attributes)
25
+ adapter.insert column_family, id, encode_attributes(attributes)
26
+ end
27
+
28
+ def update_record(id, attributes)
29
+ adapter.update column_family, id, encode_attributes(attributes)
30
+ end
31
+
32
+ def batching?
33
+ adapter.batching?
34
+ end
35
+
36
+ def batch(&block)
37
+ adapter.batch(&block)
38
+ end
39
+
40
+ def instantiate(id, attributes)
41
+ allocate.tap do |object|
42
+ object.instance_variable_set("@id", id) if id
43
+ object.instance_variable_set("@new_record", false)
44
+ object.instance_variable_set("@destroyed", false)
45
+ object.instance_variable_set("@attributes", typecast_persisted_attributes(object, attributes))
46
+ end
47
+ end
48
+
49
+ def encode_attributes(attributes)
50
+ encoded = {}
51
+ attributes.each do |column_name, value|
52
+ if value.nil?
53
+ encoded[column_name] = nil
54
+ else
55
+ encoded[column_name] = attribute_definitions[column_name].coder.encode(value)
56
+ end
57
+ end
58
+ encoded
59
+ end
60
+
61
+ private
62
+
63
+ def quote_columns(column_names)
64
+ column_names.map { |name| "'#{name}'" }
65
+ end
66
+
67
+ def typecast_persisted_attributes(object, attributes)
68
+ attributes.each do |key, value|
69
+ if definition = attribute_definitions[key]
70
+ attributes[key] = definition.instantiate(object, value)
71
+ else
72
+ attributes.delete(key)
73
+ end
74
+ end
75
+
76
+ attribute_definitions.each_value do |definition|
77
+ unless definition.default.nil? || attributes.has_key?(definition.name)
78
+ attributes[definition.name] = definition.default
79
+ end
80
+ end
81
+
82
+ attributes
83
+ end
84
+ end
85
+
86
+ def new_record?
87
+ @new_record
88
+ end
89
+
90
+ def destroyed?
91
+ @destroyed
92
+ end
93
+
94
+ def persisted?
95
+ !(new_record? || destroyed?)
96
+ end
97
+
98
+ def save(*)
99
+ new_record? ? create : update
100
+ end
101
+
102
+ def destroy
103
+ self.class.remove(id)
104
+ @destroyed = true
105
+ end
106
+
107
+ def update_attribute(name, value)
108
+ name = name.to_s
109
+ send("#{name}=", value)
110
+ save(validate: false)
111
+ end
112
+
113
+ def update_attributes(attributes)
114
+ self.attributes = attributes
115
+ save
116
+ end
117
+
118
+ def update_attributes!(attributes)
119
+ self.attributes = attributes
120
+ save!
121
+ end
122
+
123
+ def becomes(klass)
124
+ became = klass.new
125
+ became.instance_variable_set("@attributes", @attributes)
126
+ became.instance_variable_set("@new_record", new_record?)
127
+ became.instance_variable_set("@destroyed", destroyed?)
128
+ became
129
+ end
130
+
131
+ def reload
132
+ clear_belongs_to_cache
133
+ @attributes = self.class.find(id).instance_variable_get('@attributes')
134
+ self
135
+ end
136
+
137
+ private
138
+
139
+ def create
140
+ @new_record = false
141
+ write :insert_record
142
+ end
143
+
144
+ def update
145
+ write :update_record
146
+ end
147
+
148
+ def write(method)
149
+ changed_attributes = Hash[changed.map { |attr| [attr, read_attribute(attr)] }]
150
+ self.class.send(method, id, changed_attributes)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,30 @@
1
+ module Superstore
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load 'superstore/tasks/ks.rake'
5
+ end
6
+
7
+ initializer "superstore.config" do |app|
8
+ ActiveSupport.on_load :superstore do
9
+ pathname = Rails.root.join('config', 'superstore.yml')
10
+ if pathname.exist?
11
+ config = YAML.load(pathname.read)
12
+
13
+ if config = config[Rails.env]
14
+ self.config = config.symbolize_keys!
15
+ else
16
+ raise "Missing environment #{Rails.env} in superstore.yml"
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ # Expose database runtime to controller for logging.
23
+ initializer "superstore.log_runtime" do |app|
24
+ require "superstore/railties/controller_runtime"
25
+ ActiveSupport.on_load(:action_controller) do
26
+ include Superstore::Railties::ControllerRuntime
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ require 'active_support/core_ext/module/attr_internal'
2
+ require 'superstore/log_subscriber'
3
+
4
+ module Superstore
5
+ module Railties # :nodoc:
6
+ module ControllerRuntime #:nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods # :nodoc:
10
+ def log_process_action(payload)
11
+ messages, cassandra_object_runtime = super, payload[:cassandra_object_runtime]
12
+ if cassandra_object_runtime.to_i > 0
13
+ messages << ("Superstore: %.1fms" % cassandra_object_runtime.to_f)
14
+ end
15
+ messages
16
+ end
17
+ end
18
+
19
+ #private
20
+
21
+ attr_internal :cassandra_object_runtime
22
+
23
+ def process_action(action, *args)
24
+ # We also need to reset the runtime before each action
25
+ # because of queries in middleware or in cases we are streaming
26
+ # and it won't be cleaned up by the method below.
27
+ Superstore::LogSubscriber.reset_runtime
28
+ super
29
+ end
30
+
31
+ def cleanup_view_runtime
32
+ runtime_before_render = Superstore::LogSubscriber.reset_runtime
33
+ runtime = super
34
+ runtime_after_render = Superstore::LogSubscriber.reset_runtime
35
+ self.cassandra_object_runtime = runtime_before_render + runtime_after_render
36
+ runtime - runtime_after_render
37
+ end
38
+
39
+ def append_info_to_payload(payload)
40
+ super
41
+ payload[:cassandra_object_runtime] = (cassandra_object_runtime || 0) + Superstore::LogSubscriber.reset_runtime
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ module Superstore
2
+ class Schema
3
+ class << self
4
+ def create_table(table_name, options = {})
5
+ adapter.create_table table_name, options
6
+ end
7
+
8
+ def drop_table(table_name)
9
+ adapter.drop_table table_name
10
+ end
11
+
12
+ private
13
+
14
+ def adapter
15
+ Superstore::Base.adapter
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module Superstore
2
+ class Scope
3
+ module Batches
4
+ def find_each(options = {})
5
+ find_in_batches(options) do |records|
6
+ records.each { |record| yield record }
7
+ end
8
+ end
9
+
10
+ def find_in_batches(options = {})
11
+ batch_size = options.delete(:batch_size) || 1000
12
+ start_key = nil
13
+
14
+ scope = limit(batch_size + 1)
15
+ records = scope.to_a
16
+
17
+ while records.any?
18
+ if records.size > batch_size
19
+ next_record = records.pop
20
+ else
21
+ next_record = nil
22
+ end
23
+
24
+ yield records
25
+ break if next_record.nil?
26
+
27
+ records = scope.where("#{adapter.primary_key_column} >= '#{next_record.id}'").to_a
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ module Superstore
2
+ class Scope
3
+ module FinderMethods
4
+ def find(ids)
5
+ if ids.is_a?(Array)
6
+ find_some(ids)
7
+ else
8
+ find_one(ids)
9
+ end
10
+ end
11
+
12
+ def find_by_id(ids)
13
+ find(ids)
14
+ rescue Superstore::RecordNotFound
15
+ nil
16
+ end
17
+
18
+ def all
19
+ to_a
20
+ end
21
+
22
+ def first
23
+ limit(1).to_a.first
24
+ end
25
+
26
+ private
27
+
28
+ def find_one(id)
29
+ if id.blank?
30
+ raise Superstore::RecordNotFound, "Couldn't find #{self.name} with key #{id.inspect}"
31
+ elsif record = where_ids(id).first
32
+ record
33
+ else
34
+ raise Superstore::RecordNotFound
35
+ end
36
+ end
37
+
38
+ def find_some(ids)
39
+ ids = ids.flatten
40
+ return [] if ids.empty?
41
+
42
+ ids = ids.compact.map(&:to_s).uniq
43
+
44
+ where_ids(ids).to_a
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ module Superstore
2
+ class Scope
3
+ module QueryMethods
4
+ def select!(*values)
5
+ self.select_values += values.flatten
6
+ self
7
+ end
8
+
9
+ def select(*values, &block)
10
+ if block_given?
11
+ to_a.select(&block)
12
+ else
13
+ clone.select! *values
14
+ end
15
+ end
16
+
17
+ def where!(*values)
18
+ self.where_values += values.flatten
19
+ self
20
+ end
21
+
22
+ def where(*values)
23
+ clone.where! values
24
+ end
25
+
26
+ def where_ids!(*ids)
27
+ self.id_values += ids.flatten
28
+ self
29
+ end
30
+
31
+ def where_ids(*ids)
32
+ clone.where_ids! ids
33
+ end
34
+
35
+ def limit!(value)
36
+ self.limit_value = value
37
+ self
38
+ end
39
+
40
+ def limit(value)
41
+ clone.limit! value
42
+ end
43
+
44
+ def to_a
45
+ select_records
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ require 'superstore/scope/batches'
2
+ require 'superstore/scope/finder_methods'
3
+ require 'superstore/scope/query_methods'
4
+
5
+ module Superstore
6
+ class Scope
7
+ include Batches, FinderMethods, QueryMethods
8
+
9
+ attr_accessor :klass
10
+ attr_accessor :limit_value, :select_values, :where_values, :id_values
11
+
12
+ def initialize(klass)
13
+ @klass = klass
14
+
15
+ @limit_value = nil
16
+ @select_values = []
17
+ @where_values = []
18
+ @id_values = []
19
+ end
20
+
21
+ private
22
+
23
+ def scoping
24
+ previous, klass.current_scope = klass.current_scope, self
25
+ yield
26
+ ensure
27
+ klass.current_scope = previous
28
+ end
29
+
30
+ def method_missing(method_name, *args, &block)
31
+ if klass.respond_to?(method_name)
32
+ scoping { klass.send(method_name, *args, &block) }
33
+ elsif Array.method_defined?(method_name)
34
+ to_a.send(method_name, *args, &block)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def select_records
41
+ results = []
42
+ klass.adapter.select(self) do |key, attributes|
43
+ results << klass.instantiate(key, attributes)
44
+ end
45
+ results.compact!
46
+ results
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module Superstore
2
+ module Scoping
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ singleton_class.class_eval do
7
+ delegate :find, :find_by_id, :first, :all, to: :scope
8
+ delegate :find_each, :find_in_batches, to: :scope
9
+ delegate :select, :where, :where_ids, to: :scope
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def scope
15
+ self.current_scope ||= Scope.new(self)
16
+ end
17
+
18
+ def current_scope
19
+ Thread.current["#{self}_current_scope"]
20
+ end
21
+
22
+ def current_scope=(new_scope)
23
+ Thread.current["#{self}_current_scope"] = new_scope
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ ks_namespace = namespace :ks do
2
+ desc 'Create the keyspace in config/superstore.yml for the current environment'
3
+ task create: :environment do
4
+ begin
5
+ Superstore::CassandraSchema.create_keyspace Superstore::Base.config[:keyspace], Superstore::Base.config[:keyspace_options]
6
+ rescue Exception => e
7
+ if e.message =~ /conflicts/
8
+ p "Keyspace #{Superstore::Base.config[:keyspace]} already exists"
9
+ else
10
+ raise e
11
+ end
12
+ end
13
+ end
14
+
15
+ task drop: :environment do
16
+ begin
17
+ Superstore::CassandraSchema.drop_keyspace Superstore::Base.config[:keyspace]
18
+ rescue Exception => e
19
+ if e.message =~ /non existing keyspace/
20
+ p "Keyspace #{Superstore::Base.config[:keyspace]} does not exist"
21
+ else
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+
27
+ task reset: [:drop, :setup]
28
+
29
+ task setup: [:create, :_load]
30
+
31
+ namespace :structure do
32
+ task dump: :environment do
33
+ filename = ENV['SCHEMA'] || "#{Rails.root}/ks/structure.cql"
34
+ File.open(filename, "w:utf-8") do |file|
35
+ Superstore::CassandraSchema.dump(file)
36
+ end
37
+ end
38
+
39
+ task load: :environment do
40
+ filename = ENV['SCHEMA'] || "#{Rails.root}/ks/structure.cql"
41
+ File.open(filename) do |file|
42
+ Superstore::CassandraSchema.load(file)
43
+ end
44
+ end
45
+ end
46
+
47
+ task :_dump do
48
+ ks_namespace["structure:dump"].invoke
49
+ end
50
+
51
+ task :_load do
52
+ ks_namespace["structure:load"].invoke
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ module Superstore
2
+ module Timestamps
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :created_at, type: :time
7
+ attribute :updated_at, type: :time
8
+
9
+ before_create do
10
+ self.created_at ||= Time.current
11
+ self.updated_at ||= Time.current
12
+ end
13
+
14
+ before_update if: :changed? do
15
+ self.updated_at = Time.current
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module Superstore
2
+ class Type
3
+ cattr_accessor :attribute_types
4
+ self.attribute_types = {}.with_indifferent_access
5
+
6
+ class << self
7
+ def register(name, coder)
8
+ attribute_types[name] = coder
9
+ end
10
+
11
+ def get_coder(name)
12
+ attribute_types[name]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module Superstore
2
+ module Types
3
+ class ArrayType < BaseType
4
+ def encode(array)
5
+ raise ArgumentError.new("#{array.inspect} is not an Array") unless array.kind_of?(Array)
6
+ array.to_a.to_json
7
+ end
8
+
9
+ def decode(str)
10
+ return nil if str.blank?
11
+
12
+ ActiveSupport::JSON.decode(str)
13
+ end
14
+
15
+ def typecast(value)
16
+ value.to_a
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Superstore
2
+ module Types
3
+ class BaseType
4
+ attr_accessor :options
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def default
10
+ options[:default].duplicable? ? options[:default].dup : options[:default]
11
+ end
12
+
13
+ def encode(value)
14
+ value.to_s
15
+ end
16
+
17
+ def decode(str)
18
+ str
19
+ end
20
+
21
+ def typecast(value)
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Superstore
2
+ module Types
3
+ class BooleanType < BaseType
4
+ TRUE_VALS = [true, 'true', '1']
5
+ FALSE_VALS = [false, 'false', '0', '', nil]
6
+ VALID_VALS = TRUE_VALS + FALSE_VALS
7
+
8
+ def encode(bool)
9
+ unless VALID_VALS.include?(bool)
10
+ raise ArgumentError.new("#{bool.inspect} is not a Boolean")
11
+ end
12
+ TRUE_VALS.include?(bool) ? '1' : '0'
13
+ end
14
+
15
+ def decode(str)
16
+ TRUE_VALS.include?(str)
17
+ end
18
+ end
19
+ end
20
+ end