motion_record 0.0.1
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 +15 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +246 -0
- data/Rakefile +1 -0
- data/lib/motion_record/base.rb +54 -0
- data/lib/motion_record/connection_adapters/sqlite_adapter.rb +160 -0
- data/lib/motion_record/persistence.rb +99 -0
- data/lib/motion_record/schema/column_definition.rb +111 -0
- data/lib/motion_record/schema/index_definition.rb +35 -0
- data/lib/motion_record/schema/migration.rb +25 -0
- data/lib/motion_record/schema/migration_definition.rb +32 -0
- data/lib/motion_record/schema/migrator.rb +39 -0
- data/lib/motion_record/schema/migrator_definition.rb +18 -0
- data/lib/motion_record/schema/table_definition.rb +46 -0
- data/lib/motion_record/schema.rb +35 -0
- data/lib/motion_record/scope.rb +129 -0
- data/lib/motion_record/scope_helpers.rb +89 -0
- data/lib/motion_record/serialization/base_serializer.rb +20 -0
- data/lib/motion_record/serialization/boolean_serializer.rb +26 -0
- data/lib/motion_record/serialization/default_serializer.rb +14 -0
- data/lib/motion_record/serialization/json_serializer.rb +38 -0
- data/lib/motion_record/serialization/time_serializer.rb +84 -0
- data/lib/motion_record/serialization.rb +68 -0
- data/lib/motion_record/version.rb +3 -0
- data/lib/motion_record.rb +37 -0
- data/motion_record.gemspec +23 -0
- metadata +99 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
class ColumnDefinition
|
4
|
+
TYPE_MAP = {
|
5
|
+
:integer => "INTEGER",
|
6
|
+
:text => "TEXT",
|
7
|
+
:float => "REAL"
|
8
|
+
}
|
9
|
+
INVERSE_TYPE_MAP = {
|
10
|
+
"INTEGER" => :integer,
|
11
|
+
"TEXT" => :text,
|
12
|
+
"REAL" => :float
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :type
|
16
|
+
attr_reader :name
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
# name - name of the column
|
20
|
+
# type - a Symbol representing the column type
|
21
|
+
# options - Hash of constraints for the column:
|
22
|
+
# :primary - set to true to configure as the primary auto-incrementing key
|
23
|
+
# :null - set to false to add a "NOT NULL" constraint
|
24
|
+
# :default - TODO
|
25
|
+
def initialize(type, name, options)
|
26
|
+
@type = type.to_sym
|
27
|
+
@name = name.to_sym
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_sql_definition
|
32
|
+
[@name, sql_type, sql_options].compact.join(" ")
|
33
|
+
end
|
34
|
+
|
35
|
+
def default
|
36
|
+
@options[:default]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Build a new ColumnDefinition from the result of a "PRAGMA table_info()"
|
40
|
+
# query
|
41
|
+
#
|
42
|
+
# pragma - Hash representing a row of the query's result:
|
43
|
+
# :cid - column index
|
44
|
+
# :name - column name
|
45
|
+
# :type - column type
|
46
|
+
# :notnull - integer flag for "NOT NULL"
|
47
|
+
# :dflt_value - default value
|
48
|
+
# :pk - integer flag for primary key
|
49
|
+
#
|
50
|
+
# Returns the new ColumnDefinition
|
51
|
+
def self.from_pragma(pragma)
|
52
|
+
type = INVERSE_TYPE_MAP[pragma[:type]]
|
53
|
+
options = {
|
54
|
+
:null => (pragma[:notnull] != 1),
|
55
|
+
:primary => (pragma[:pk] == 1),
|
56
|
+
:default => (pragma[:dflt_value])
|
57
|
+
}
|
58
|
+
|
59
|
+
if options[:default]
|
60
|
+
case type
|
61
|
+
when :integer
|
62
|
+
options[:default] = options[:default].to_i
|
63
|
+
when :float
|
64
|
+
options[:default] = options[:default].to_f
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
self.new(type, pragma[:name], options)
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def sql_type
|
74
|
+
if TYPE_MAP[@type]
|
75
|
+
TYPE_MAP[@type]
|
76
|
+
else
|
77
|
+
raise "Unrecognized column type: #{@type.inspect}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def sql_options
|
82
|
+
sql_options = []
|
83
|
+
|
84
|
+
@options.each do |key, value|
|
85
|
+
case key
|
86
|
+
when :primary
|
87
|
+
if value
|
88
|
+
sql_options << "PRIMARY KEY ASC AUTOINCREMENT"
|
89
|
+
end
|
90
|
+
when :null
|
91
|
+
if !value
|
92
|
+
sql_options << "NOT NULL"
|
93
|
+
end
|
94
|
+
when :default
|
95
|
+
if value
|
96
|
+
sql_options << "DEFAULT #{value.inspect}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
raise "Unrecognized column option: #{key.inspect}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
if sql_options.any?
|
104
|
+
sql_options.join(" ")
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
class IndexDefinition
|
4
|
+
|
5
|
+
# Initialize the index definition
|
6
|
+
#
|
7
|
+
# table_name - name of the table
|
8
|
+
# columns - either the String name of the column to index or an Array
|
9
|
+
# of column names
|
10
|
+
# options - optional Hash of options for the index
|
11
|
+
# :unique - set to true to create a unique index
|
12
|
+
# :name - provide a String to override the default index name
|
13
|
+
def initialize(table_name, columns, options={})
|
14
|
+
@table_name = table_name
|
15
|
+
@columns = columns.is_a?(Array) ? columns : [columns]
|
16
|
+
|
17
|
+
@name = options[:name] || build_name_from_columns
|
18
|
+
@unique = !!options[:unique]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add the index to the database
|
22
|
+
def execute
|
23
|
+
index_statement = "CREATE#{' UNIQUE' if @unique} INDEX #{@name} ON #{@table_name} (#{@columns.join ", "})"
|
24
|
+
|
25
|
+
MotionRecord::Base.connection.execute index_statement
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def build_name_from_columns
|
31
|
+
"index_#{@table_name}_on_#{@columns.join "_and_"}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# MotionRecord::Schema::Migration represents versions of migrations which have
|
2
|
+
# been run
|
3
|
+
|
4
|
+
module MotionRecord
|
5
|
+
module Schema
|
6
|
+
class Migration < Base
|
7
|
+
class << self
|
8
|
+
def table_name
|
9
|
+
"schema_migrations"
|
10
|
+
end
|
11
|
+
|
12
|
+
def table_exists?
|
13
|
+
connection.table_exists?(table_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_table
|
17
|
+
table = Schema::TableDefinition.new(table_name, id: false)
|
18
|
+
table.integer :version, :null => false
|
19
|
+
table.index :version, :unique => true
|
20
|
+
table.execute
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
class MigrationDefinition
|
4
|
+
attr_reader :version
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(version, name = nil)
|
8
|
+
@version = version.to_i
|
9
|
+
@name = name || "Migration ##{@version}"
|
10
|
+
@definitions = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
@definitions.each(&:execute)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_table(name, options = {})
|
18
|
+
table_definition = TableDefinition.new(name, options)
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
yield table_definition
|
22
|
+
end
|
23
|
+
|
24
|
+
@definitions << table_definition
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_index(name, columns, options = {})
|
28
|
+
@definitions << IndexDefinition.new(name, columns, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
class Migrator
|
4
|
+
|
5
|
+
attr_reader :migrations
|
6
|
+
|
7
|
+
def initialize(migrations)
|
8
|
+
@migrations = migrations
|
9
|
+
@migrated_versions = nil
|
10
|
+
|
11
|
+
initialize_schema_table
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
pending_migrations.each do |migration|
|
16
|
+
migration.execute
|
17
|
+
@migrated_versions << migration.version
|
18
|
+
Schema::Migration.create!(version: migration.version)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def pending_migrations
|
23
|
+
@migrations.reject { |migration| migrated.include?(migration.version) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def migrated
|
27
|
+
@migrated_versions ||= Schema::Migration.pluck(:version).sort
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def initialize_schema_table
|
33
|
+
unless Schema::Migration.table_exists?
|
34
|
+
Schema::Migration.create_table
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# DSL helper for defining migrations
|
2
|
+
module MotionRecord
|
3
|
+
module Schema
|
4
|
+
class MigratorDefinition
|
5
|
+
attr_reader :migrations
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@migrations = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def migration(version, name=nil, &block)
|
12
|
+
migration_definition = Schema::MigrationDefinition.new(version, name)
|
13
|
+
migration_definition.instance_eval &block
|
14
|
+
@migrations << migration_definition
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
class TableDefinition
|
4
|
+
def initialize(name, options={})
|
5
|
+
@name = name
|
6
|
+
@columns = []
|
7
|
+
@index_definitions = []
|
8
|
+
|
9
|
+
unless options.has_key?(:id) && !options[:id]
|
10
|
+
add_default_primary_column
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
# Create table
|
16
|
+
column_sql = @columns.map(&:to_sql_definition).join(", ")
|
17
|
+
MotionRecord::Base.connection.execute "CREATE TABLE #{@name} (#{column_sql})"
|
18
|
+
|
19
|
+
# Create table's indexes
|
20
|
+
@index_definitions.each(&:execute)
|
21
|
+
end
|
22
|
+
|
23
|
+
def text(column_name, options={})
|
24
|
+
@columns << ColumnDefinition.new(:text, column_name, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def integer(column_name, options={})
|
28
|
+
@columns << ColumnDefinition.new(:integer, column_name, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def float(column_name, options={})
|
32
|
+
@columns << ColumnDefinition.new(:float, column_name, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def index(columns, options={})
|
36
|
+
@index_definitions << IndexDefinition.new(@name, columns, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def add_default_primary_column
|
42
|
+
@columns << ColumnDefinition.new(:integer, "id", primary: true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Schema
|
3
|
+
# Define and run all pending migrations (should be done during app setup)
|
4
|
+
#
|
5
|
+
# options - Hash of configuration options for the SQLite connection
|
6
|
+
# :file - full name of the database file, or :memory for in-memory
|
7
|
+
# database files (default is "app.sqlite3" in the app's
|
8
|
+
# `/Library/Application Support` folder)
|
9
|
+
# :debug - set to false to turn off SQL debug logging
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# MotionRecord::Schema.up! do
|
14
|
+
# migration 1, "Create events table" do
|
15
|
+
# create_table "events" do |t|
|
16
|
+
# t.text :name, :null => false
|
17
|
+
# t.text :properties
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# migration 2, "Index events table" do
|
22
|
+
# add_index "events", "name", :unique => true
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
def self.up!(options={}, &block)
|
27
|
+
ConnectionAdapters::SQLiteAdapter.configure(options)
|
28
|
+
|
29
|
+
definition = Schema::MigratorDefinition.new
|
30
|
+
definition.instance_eval &block
|
31
|
+
|
32
|
+
Schema::Migrator.new(definition.migrations).run
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# A model for building scoped database queries
|
2
|
+
#
|
3
|
+
|
4
|
+
module MotionRecord
|
5
|
+
class Scope
|
6
|
+
|
7
|
+
attr_reader :klass
|
8
|
+
attr_reader :conditions
|
9
|
+
|
10
|
+
def initialize(klass, options = {})
|
11
|
+
@klass = klass
|
12
|
+
@conditions = options[:conditions] || {} # TODO: freeze?
|
13
|
+
@order = options[:order]
|
14
|
+
@limit = options[:limit]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Scope builder
|
18
|
+
|
19
|
+
def where(conditions={})
|
20
|
+
Scope.new(@klass, :conditions => @conditions.merge(conditions), :order => @order, :limit => @limit)
|
21
|
+
end
|
22
|
+
|
23
|
+
def order(ordering_term)
|
24
|
+
Scope.new(@klass, :conditions => @conditions, :order => ordering_term, :limit => @limit)
|
25
|
+
end
|
26
|
+
|
27
|
+
def limit(limit_value)
|
28
|
+
Scope.new(@klass, :conditions => @conditions, :order => @order, :limit => limit_value)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read-only queries
|
32
|
+
|
33
|
+
def exists?
|
34
|
+
count > 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def first
|
38
|
+
limit(1).find_all.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def find(id)
|
42
|
+
self.where(@klass.primary_key => id).first
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_all
|
46
|
+
connection.select(self).map { |row| @klass.from_table_params(row) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def pluck(attribute)
|
50
|
+
connection.select(self).map { |row| row[attribute] }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Persistence queries
|
54
|
+
|
55
|
+
def update_all(params)
|
56
|
+
connection.update(self, params)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_all
|
60
|
+
connection.delete(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Calculations
|
64
|
+
|
65
|
+
def count(column=nil)
|
66
|
+
calculate(:count, column)
|
67
|
+
end
|
68
|
+
|
69
|
+
def maximum(column)
|
70
|
+
calculate(:maximum, column)
|
71
|
+
end
|
72
|
+
|
73
|
+
def minimum(column)
|
74
|
+
calculate(:minimum, column)
|
75
|
+
end
|
76
|
+
|
77
|
+
def sum(column)
|
78
|
+
calculate(:sum, column)
|
79
|
+
end
|
80
|
+
|
81
|
+
def average(column)
|
82
|
+
calculate(:average, column)
|
83
|
+
end
|
84
|
+
|
85
|
+
# SQL helpers
|
86
|
+
|
87
|
+
def predicate?
|
88
|
+
predicate_segments.any?
|
89
|
+
end
|
90
|
+
|
91
|
+
def predicate
|
92
|
+
predicate_segments.join(" ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def predicate_values
|
96
|
+
condition_columns.map { |column| @conditions[column] }
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def calculate(method, column)
|
102
|
+
connection.calculate(self, method, column)
|
103
|
+
end
|
104
|
+
|
105
|
+
def predicate_segments
|
106
|
+
unless @predicate_segments
|
107
|
+
@predicate_segments = []
|
108
|
+
if condition_columns.any?
|
109
|
+
@predicate_segments << "WHERE #{condition_columns.map { |c| "#{c} = ? " }.join " AND "}"
|
110
|
+
end
|
111
|
+
if @order
|
112
|
+
@predicate_segments << "ORDER BY #{@order}"
|
113
|
+
end
|
114
|
+
if @limit
|
115
|
+
@predicate_segments << "LIMIT #{@limit}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
@predicate_segments
|
119
|
+
end
|
120
|
+
|
121
|
+
def condition_columns
|
122
|
+
@condition_columns ||= @conditions.keys
|
123
|
+
end
|
124
|
+
|
125
|
+
def connection
|
126
|
+
Base.connection
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# These helper methods make it possible to call scope methods like `where` and
|
2
|
+
# `find` directly on MotionRecord::Base classes
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
#
|
6
|
+
# Event.where(:name => "launched").find_all
|
7
|
+
#
|
8
|
+
# FIXME: MotionRecord::Persistence includes this module, but the normal file
|
9
|
+
# ordering breaks the dependency
|
10
|
+
module MotionRecord
|
11
|
+
module ScopeHelpers
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
# Read-only queries
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
scoped.exists?
|
18
|
+
end
|
19
|
+
|
20
|
+
def first
|
21
|
+
scoped.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(id)
|
25
|
+
scoped.find(id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_all
|
29
|
+
scoped.find_all
|
30
|
+
end
|
31
|
+
|
32
|
+
def pluck(attribute)
|
33
|
+
scoped.pluck(attribute)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Persistence queries
|
37
|
+
|
38
|
+
def update_all(params)
|
39
|
+
scoped.update_all(params)
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete_all
|
43
|
+
scoped.delete_all
|
44
|
+
end
|
45
|
+
|
46
|
+
# Calculations
|
47
|
+
|
48
|
+
def count(column=nil)
|
49
|
+
scoped.count(column)
|
50
|
+
end
|
51
|
+
|
52
|
+
def maximum(column)
|
53
|
+
scoped.maximum(column)
|
54
|
+
end
|
55
|
+
|
56
|
+
def minimum(column)
|
57
|
+
scoped.minimum(column)
|
58
|
+
end
|
59
|
+
|
60
|
+
def sum(column)
|
61
|
+
scoped.sum(column)
|
62
|
+
end
|
63
|
+
|
64
|
+
def average(column)
|
65
|
+
scoped.average(column)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Scope building
|
69
|
+
|
70
|
+
def where(conditions={})
|
71
|
+
scoped.where(conditions)
|
72
|
+
end
|
73
|
+
|
74
|
+
def order(ordering_term)
|
75
|
+
scoped.order(ordering_term)
|
76
|
+
end
|
77
|
+
|
78
|
+
def limit(limit_value)
|
79
|
+
scoped.limit(limit_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def scoped
|
85
|
+
Scope.new(self)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Serialization
|
3
|
+
class BaseSerializer
|
4
|
+
# column - a Schema::ColumnDefinition object
|
5
|
+
def initialize(column)
|
6
|
+
@column = column
|
7
|
+
end
|
8
|
+
|
9
|
+
# Override this method in a subclass to define the custom serializer
|
10
|
+
def serialize(value)
|
11
|
+
raise "Must be implemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override this method in a subclass to define the custom serializer
|
15
|
+
def deserialize(value)
|
16
|
+
raise "Must be implemented"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Serialization
|
3
|
+
class BooleanSerializer < BaseSerializer
|
4
|
+
|
5
|
+
def serialize(value)
|
6
|
+
if @column.type == :integer
|
7
|
+
value ? 1 : 0
|
8
|
+
else
|
9
|
+
raise "Can't serialize #{value.inspect} to #{@column.type.inspect}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(value)
|
14
|
+
if @column.type == :integer
|
15
|
+
if value == 0 || value.nil?
|
16
|
+
false
|
17
|
+
else
|
18
|
+
true
|
19
|
+
end
|
20
|
+
else
|
21
|
+
raise "Can't deserialize #{value.inspect} from #{@column.type.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Serialization
|
3
|
+
class JSONParserError < StandardError; end
|
4
|
+
|
5
|
+
class JSONSerializer < BaseSerializer
|
6
|
+
|
7
|
+
def serialize(value)
|
8
|
+
unless @column.type == :text
|
9
|
+
raise "JSON can only be serialized to TEXT columns"
|
10
|
+
end
|
11
|
+
self.class.generate_json(value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def deserialize(value)
|
15
|
+
unless @column.type == :text
|
16
|
+
raise "JSON can only be deserialized from TEXT columns"
|
17
|
+
end
|
18
|
+
self.class.parse_json(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
# JSON generate/parse code is hoisted from BubbleWrap::JSON
|
22
|
+
|
23
|
+
def self.generate_json(obj)
|
24
|
+
NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil).to_str
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.parse_json(str_data)
|
28
|
+
return nil unless str_data
|
29
|
+
data = str_data.respond_to?('dataUsingEncoding:') ? str_data.dataUsingEncoding(NSUTF8StringEncoding) : str_data
|
30
|
+
opts = NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments
|
31
|
+
error = Pointer.new(:id)
|
32
|
+
obj = NSJSONSerialization.JSONObjectWithData(data, options:opts, error:error)
|
33
|
+
raise JSONParserError, error[0].description if error[0]
|
34
|
+
obj
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|