scorpio 0.2.3 → 0.3.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.
@@ -1,127 +0,0 @@
1
- module Scorpio
2
- # base class for representing an instance of an instance described by a schema
3
- class SchemaInstanceBase
4
- class << self
5
- def class_comment
6
- lines = []
7
-
8
- description = schema &&
9
- schema['description'].respond_to?(:to_str) &&
10
- schema['description'].to_str
11
- if description
12
- description.split("\n", -1).each do |descline|
13
- lines << "# " + descline
14
- end
15
- lines << "#"
16
- end
17
-
18
- schema.described_hash_property_names.each_with_index do |propname, i|
19
- lines << "#" unless i == 0
20
- lines << "# @!attribute [rw] #{propname}"
21
-
22
- property_schema = schema['properties'].respond_to?(:to_hash) &&
23
- schema['properties'][propname].respond_to?(:to_hash) &&
24
- schema['properties'][propname]
25
-
26
- required = property_schema && property_schema['required']
27
- required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
28
- lines << "# @required" if required
29
-
30
- type = property_schema &&
31
- property_schema['type'].respond_to?(:to_str) &&
32
- property_schema['type'].to_str
33
- simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
34
- rettypes = []
35
- if simple.key?(type)
36
- rettypes << simple[type]
37
- elsif type == 'object' || type == 'array'
38
- rettypes = []
39
- schema_class = Scorpio.class_for_schema(property_schema)
40
- unless schema_class.name =~ /\AScorpio::SchemaClasses::/
41
- rettypes << schema_class.name
42
- end
43
- rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
44
- elsif type
45
- # not really valid, but there's some information in there. whatever it is.
46
- rettypes << type
47
- end
48
- # we'll add Object to all because the accessor methods have no enforcement that their value is
49
- # of the specified type, and may return anything really. TODO: consider if this is of any value?
50
- rettypes << 'Object'
51
- lines << "# @return [#{rettypes.join(', ')}]"
52
-
53
- description = property_schema &&
54
- property_schema['description'].respond_to?(:to_str) &&
55
- property_schema['description'].to_str
56
- if description
57
- description.split("\n", -1).each do |descline|
58
- lines << "# " + descline
59
- end
60
- end
61
- end
62
- lines.join("\n")
63
- end
64
-
65
- def to_rb
66
- lines = []
67
- description = schema &&
68
- schema['description'].respond_to?(:to_str) &&
69
- schema['description'].to_str
70
- if description
71
- description.split("\n", -1).each do |descline|
72
- lines << "# " + descline
73
- end
74
- end
75
- lines << "class #{name}"
76
- schema.described_hash_property_names.each_with_index do |propname, i|
77
- lines << "" unless i == 0
78
- property_schema = schema['properties'].respond_to?(:to_hash) &&
79
- schema['properties'][propname].respond_to?(:to_hash) &&
80
- schema['properties'][propname]
81
- description = property_schema &&
82
- property_schema['description'].respond_to?(:to_str) &&
83
- property_schema['description'].to_str
84
- if description
85
- description.split("\n", -1).each do |descline|
86
- lines << " # " + descline
87
- end
88
- lines << " #" # blank comment line between description and @return
89
- end
90
-
91
- required = property_schema && property_schema['required']
92
- required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
93
- lines << " # @required" if required
94
-
95
- type = property_schema &&
96
- property_schema['type'].respond_to?(:to_str) &&
97
- property_schema['type'].to_str
98
- simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
99
- rettypes = []
100
- if simple.key?(type)
101
- rettypes << simple[type]
102
- elsif type == 'object' || type == 'array'
103
- rettypes = []
104
- schema_class = Scorpio.class_for_schema(property_schema)
105
- unless schema_class.name =~ /\AScorpio::SchemaClasses::/
106
- rettypes << schema_class.name
107
- end
108
- rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
109
- elsif type
110
- # not really valid, but there's some information in there. whatever it is.
111
- rettypes << type
112
- end
113
- # we'll add Object to all because the accessor methods have no enforcement that their value is
114
- # of the specified type, and may return anything really. TODO: consider if this is of any value?
115
- rettypes << 'Object'
116
- lines << " # @return [#{rettypes.join(', ')}]"
117
-
118
- lines << " def #{propname}"
119
- lines << " super"
120
- lines << " end"
121
- end
122
- lines << "end"
123
- lines.join("\n")
124
- end
125
- end
126
- end
127
- end
@@ -1,83 +0,0 @@
1
- module Scorpio
2
- # this is a ActiveRecord serialization class intended to store JSON in the
3
- # database column and expose a ruby class once loaded on a model instance.
4
- # this allows for better ruby idioms to access to properties, and definition
5
- # of related methods on the loaded class.
6
- #
7
- # the first argument, `loaded_class`, is the class which will be used to
8
- # instantiate the column data. properties of the loaded class will correspond
9
- # to keys of the json object in the database.
10
- #
11
- # the column data may be either a single instance of the loaded class
12
- # (represented as one json object) or an array of them (represented as a json
13
- # array of json objects), indicated by the keyword argument `array`.
14
- #
15
- # the column behind the attribute may be an actual JSON column (postgres json
16
- # or jsonb - hstore should work too if you only have string attributes) or a
17
- # serialized string, indicated by the keyword argument `string`.
18
- class ObjectJSONCoder
19
- class Error < StandardError
20
- end
21
- class LoadError < Error
22
- end
23
- class DumpError < Error
24
- end
25
-
26
- def initialize(loaded_class, string: false, array: false, next_coder: nil)
27
- @loaded_class = loaded_class
28
- # this notes the order of the keys as they were in the json, used by dump_object to generate
29
- # json that is equivalent to the json/jsonifiable that came in, so that AR's #changed_attributes
30
- # can tell whether the attribute has been changed.
31
- @loaded_class.send(:attr_accessor, :object_json_coder_keys_order)
32
- @string = string
33
- @array = array
34
- @next_coder = next_coder
35
- end
36
-
37
- def load(column_data)
38
- return nil if column_data.nil?
39
- data = @string ? ::JSON.parse(column_data) : column_data
40
- object = if @array
41
- unless data.respond_to?(:to_ary)
42
- raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
43
- end
44
- data.map { |el| load_object(el) }
45
- else
46
- load_object(data)
47
- end
48
- object = @next_coder.load(object) if @next_coder
49
- object
50
- end
51
-
52
- def dump(object)
53
- object = @next_coder.dump(object) if @next_coder
54
- return nil if object.nil?
55
- jsonifiable = begin
56
- if @array
57
- unless object.respond_to?(:to_ary)
58
- raise DumpError, "expected array-like attribute; got: #{object.class}: #{object.inspect}"
59
- end
60
- object.map do |el|
61
- dump_object(el)
62
- end
63
- else
64
- dump_object(object)
65
- end
66
- end
67
- @string ? ::JSON.generate(jsonifiable) : jsonifiable
68
- end
69
- end
70
- # this is a ActiveRecord serialization class intended to store JSON in the
71
- # database column and expose a given SchemaInstanceBase subclass once loaded
72
- # on a model instance.
73
- class SchemaInstanceJSONCoder < ObjectJSONCoder
74
- private
75
- def load_object(data)
76
- @loaded_class.new(data)
77
- end
78
-
79
- def dump_object(object)
80
- Scorpio::Typelike.as_json(object)
81
- end
82
- end
83
- end
@@ -1,30 +0,0 @@
1
- module Scorpio
2
- # this is a ActiveRecord serialization class intended to store JSON in the
3
- # database column and expose a Struct subclass once loaded on a model instance.
4
- class StructJSONCoder < ObjectJSONCoder
5
- private
6
- def load_object(data)
7
- if data.is_a?(Hash)
8
- good_keys = @loaded_class.members.map(&:to_s)
9
- bad_keys = data.keys - good_keys
10
- unless bad_keys.empty?
11
- raise LoadError, "expected keys #{good_keys}; got unrecognized keys: #{bad_keys}"
12
- end
13
- instance = @loaded_class.new(*@loaded_class.members.map { |m| data[m.to_s] })
14
- instance.object_json_coder_keys_order = data.keys
15
- instance
16
- else
17
- raise LoadError, "expected instance(s) of #{Hash}; got: #{data.class}: #{data.inspect}"
18
- end
19
- end
20
-
21
- def dump_object(object)
22
- if object.is_a?(@loaded_class)
23
- keys = (object.object_json_coder_keys_order || []) | @loaded_class.members.map(&:to_s)
24
- keys.map { |member| {member => object[member]} }.inject({}, &:update)
25
- else
26
- raise TypeError, "expected instance(s) of #{@loaded_class}; got: #{object.class}: #{object.inspect}"
27
- end
28
- end
29
- end
30
- end
@@ -1,164 +0,0 @@
1
- module Scorpio
2
- module Typelike
3
- def self.modified_copy(other, &block)
4
- if other.respond_to?(:modified_copy)
5
- other.modified_copy(&block)
6
- else
7
- return yield(other)
8
- end
9
- end
10
-
11
- # I could require 'json/add/core' and use #as_json but I like this better.
12
- def self.as_json(object, *opt)
13
- if object.respond_to?(:to_hash)
14
- object.map do |k, v|
15
- unless k.is_a?(Symbol) || k.respond_to?(:to_str)
16
- raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
17
- end
18
- {k.to_s => as_json(v, *opt)}
19
- end.inject({}, &:update)
20
- elsif object.respond_to?(:to_ary)
21
- object.map { |e| as_json(e, *opt) }
22
- elsif [String, TrueClass, FalseClass, NilClass, Numeric].any? { |c| object.is_a?(c) }
23
- object
24
- elsif object.is_a?(Symbol)
25
- object.to_s
26
- elsif object.is_a?(Set)
27
- as_json(object.to_a, *opt)
28
- elsif object.respond_to?(:as_json)
29
- as_json(object.as_json(*opt), *opt)
30
- else
31
- raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}")
32
- end
33
- end
34
- end
35
- module Hashlike
36
- # safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
37
- # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
38
-
39
- # methods which do not need to access the value.
40
- SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size)
41
- SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at)
42
- DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift)
43
- # these return a modified copy
44
- safe_modified_copy_methods = %w(compact merge)
45
- # select and reject will return a modified copy but need the yielded block variable value from #[]
46
- safe_kv_block_modified_copy_methods = %w(select reject)
47
- SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
48
- safe_to_hash_methods = SAFE_METHODS - safe_modified_copy_methods - safe_kv_block_modified_copy_methods
49
- safe_to_hash_methods.each do |method_name|
50
- define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
51
- end
52
- safe_modified_copy_methods.each do |method_name|
53
- define_method(method_name) do |*a, &b|
54
- Scorpio::Typelike.modified_copy(self) do |object_to_modify|
55
- object_to_modify.public_send(method_name, *a, &b)
56
- end
57
- end
58
- end
59
- safe_kv_block_modified_copy_methods.each do |method_name|
60
- define_method(method_name) do |*a, &b|
61
- Scorpio::Typelike.modified_copy(self) do |object_to_modify|
62
- object_to_modify.public_send(method_name, *a) do |k, _v|
63
- b.call(k, self[k])
64
- end
65
- end
66
- end
67
- end
68
-
69
- def inspect
70
- object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
71
- "\#{<#{self.class.to_s}#{object_group_text}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
72
- end
73
-
74
- def to_s
75
- inspect
76
- end
77
-
78
- def pretty_print(q)
79
- q.instance_exec(self) do |obj|
80
- object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
81
- text "\#{<#{obj.class.to_s}#{object_group_text}>"
82
- group_sub {
83
- nest(2) {
84
- breakable(obj.any? { true } ? ' ' : '')
85
- seplist(obj, nil, :each_pair) { |k, v|
86
- group {
87
- pp k
88
- text ' => '
89
- pp v
90
- }
91
- }
92
- }
93
- }
94
- breakable ''
95
- text '}'
96
- end
97
- end
98
- end
99
- module Arraylike
100
- # safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
101
- # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
102
-
103
- # methods which do not need to access the element.
104
- SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
105
- # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
106
- SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev assoc at bsearch bsearch_index combination compact count cycle dig drop drop_while fetch find_index first include? index join last pack permutation product reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip)
107
- DESTRUCTIVE_METHODS = %w(<< clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift)
108
-
109
- # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
110
- safe_modified_copy_methods = %w(compact)
111
-
112
- # methods that return a modified copy and do need handling of block variables
113
- safe_el_block_methods = %w(reject select)
114
-
115
- SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
116
- safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
117
- safe_to_ary_methods.each do |method_name|
118
- define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
119
- end
120
- safe_modified_copy_methods.each do |method_name|
121
- define_method(method_name) do |*a, &b|
122
- Scorpio::Typelike.modified_copy(self) do |object_to_modify|
123
- object_to_modify.public_send(method_name, *a, &b)
124
- end
125
- end
126
- end
127
- safe_el_block_methods.each do |method_name|
128
- define_method(method_name) do |*a, &b|
129
- Scorpio::Typelike.modified_copy(self) do |object_to_modify|
130
- i = 0
131
- object_to_modify.public_send(method_name, *a) do |_e|
132
- b.call(self[i]).tap { i += 1 }
133
- end
134
- end
135
- end
136
- end
137
-
138
- def inspect
139
- object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
140
- "\#[<#{self.class.to_s}#{object_group_text}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
141
- end
142
-
143
- def to_s
144
- inspect
145
- end
146
-
147
- def pretty_print(q)
148
- q.instance_exec(self) do |obj|
149
- object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
150
- text "\#[<#{obj.class.to_s}#{object_group_text}>"
151
- group_sub {
152
- nest(2) {
153
- breakable(obj.any? { true } ? ' ' : '')
154
- seplist(obj, nil, :each) { |e|
155
- pp e
156
- }
157
- }
158
- }
159
- breakable ''
160
- text ']'
161
- end
162
- end
163
- end
164
- end
@@ -1,89 +0,0 @@
1
- module Scorpio
2
- module Util
3
- def stringify_symbol_keys(hash)
4
- unless hash.respond_to?(:to_hash)
5
- raise(ArgumentError, "expected argument to be a hash; got #{hash.class.inspect}: #{hash.pretty_inspect.chomp}")
6
- end
7
- Scorpio::Typelike.modified_copy(hash) do |hash_|
8
- changed = false
9
- out = {}
10
- hash_.each do |k, v|
11
- if k.is_a?(Symbol)
12
- changed = true
13
- k = k.to_s
14
- end
15
- out[k] = v
16
- end
17
- changed ? out : hash_
18
- end
19
- end
20
-
21
- def deep_stringify_symbol_keys(object)
22
- if object.respond_to?(:to_hash)
23
- Scorpio::Typelike.modified_copy(object) do |hash|
24
- changed = false
25
- out = {}
26
- hash.each do |k, v|
27
- if k.is_a?(Symbol)
28
- changed = true
29
- k = k.to_s
30
- end
31
- out_k = deep_stringify_symbol_keys(k)
32
- out_v = deep_stringify_symbol_keys(v)
33
- changed = true if out_k.object_id != k.object_id
34
- changed = true if out_v.object_id != v.object_id
35
- out[out_k] = out_v
36
- end
37
- changed ? out : hash
38
- end
39
- elsif object.respond_to?(:to_ary)
40
- Scorpio::Typelike.modified_copy(object) do |ary|
41
- changed = false
42
- out = ary.map do |e|
43
- out_e = deep_stringify_symbol_keys(e)
44
- changed = true if out_e.object_id != e.object_id
45
- out_e
46
- end
47
- changed ? out : ary
48
- end
49
- else
50
- object
51
- end
52
- end
53
- end
54
- extend Util
55
-
56
- module FingerprintHash
57
- def ==(other)
58
- object_id == other.object_id || (other.respond_to?(:fingerprint) && other.fingerprint == self.fingerprint)
59
- end
60
-
61
- alias_method :eql?, :==
62
-
63
- def hash
64
- fingerprint.hash
65
- end
66
- end
67
-
68
- module Memoize
69
- def memoize(key, *args_)
70
- @memos ||= {}
71
- @memos[key] ||= Hash.new do |h, args|
72
- h[args] = yield(*args)
73
- end
74
- @memos[key][args_]
75
- end
76
-
77
- def clear_memo(key, *args)
78
- @memos ||= {}
79
- if @memos[key]
80
- if args.empty?
81
- @memos[key].clear
82
- else
83
- @memos[key].delete(args)
84
- end
85
- end
86
- end
87
- end
88
- extend Memoize
89
- end