emery 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8dc449ad3ba135140479a6711b016e63bfc30f9fd6dcf6dba8ff78ee71208a6a
4
+ data.tar.gz: 9c2f86bde90521cfa5adb78eda8ceef5504c2fbcf44de558cf2450c550cea96e
5
+ SHA512:
6
+ metadata.gz: e9695fe9eeed44ad9d3bc33e1dc92ae4de5a31e024c7fa9ba1fa34dbd3df554cf5cd73e9634dbaec1246ca80b8ac536fd1020b129ef26d9973512c34ca77b920
7
+ data.tar.gz: 8f7b8961c1c39cf2a354163725bd1fc761e620627ebbb73cb1dd91c7d5746bdc6fd937ccc17fa2bf70ef7af826dd0eb60f09102a4b27ab7f6cf1343633686ecf
data/lib/emery.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'emery/tod'
2
+ require 'emery/type'
3
+ require 'emery/jsoner'
4
+ require 'emery/enum'
5
+ require 'emery/dataclass'
6
+ require 'emery/client'
@@ -0,0 +1,86 @@
1
+ module Emery
2
+ module DataClass
3
+ def initialize(params)
4
+ self.class.json_attributes.each do |attr, attr_type|
5
+ attr_value = params[attr]
6
+ self.instance_variable_set("@#{attr}", T.check_var(attr, attr_type, attr_value))
7
+ end
8
+ end
9
+
10
+ def ==(other)
11
+ begin
12
+ T.check(self.class, other)
13
+ self.class.json_attributes.keys.each do |attr|
14
+ if self.instance_variable_get("@#{attr}") != other.instance_variable_get("@#{attr}")
15
+ return false
16
+ end
17
+ end
18
+ return true
19
+ rescue
20
+ return false
21
+ end
22
+ end
23
+
24
+ def copy(params)
25
+ params.each do |attr, attr_value|
26
+ if !self.class.json_attributes.key?(attr)
27
+ raise TypeError.new("Non existing attribute #{attr}")
28
+ end
29
+ end
30
+ new_params =
31
+ self.class.json_attributes.map do |attr, attr_type|
32
+ attr_value =
33
+ if params.key?(attr)
34
+ params[attr]
35
+ else
36
+ self.instance_variable_get("@#{attr}")
37
+ end
38
+ [attr, attr_value]
39
+ end.to_h
40
+ return self.class.new(new_params)
41
+ end
42
+
43
+ def self.included(base)
44
+ base.extend ClassMethods
45
+ end
46
+
47
+ module ClassMethods
48
+ def json_attributes
49
+ @json_attributes
50
+ end
51
+
52
+ def val(name, type)
53
+ if @json_attributes == nil
54
+ @json_attributes = {}
55
+ end
56
+ @json_attributes[name] = type
57
+ attr_reader name
58
+ end
59
+
60
+ def var(name, type)
61
+ if @json_attributes == nil
62
+ @json_attributes = {}
63
+ end
64
+ @json_attributes[name] = type
65
+ attr_accessor name
66
+ end
67
+
68
+ def jsoner_deserialize(json_value)
69
+ T.check(T.hash(String, NilableUntyped), json_value)
70
+ parameters = @json_attributes.map do |attr, attr_type|
71
+ attr_value = json_value[attr.to_s]
72
+ [attr, Jsoner.deserialize(attr_type, attr_value)]
73
+ end
74
+ return self.new parameters.to_h
75
+ end
76
+
77
+ def jsoner_serialize(value)
78
+ T.check(self, value)
79
+ attrs = @json_attributes.map do |attr, attr_type|
80
+ [attr, Jsoner.serialize(attr_type, value.send(attr))]
81
+ end
82
+ return attrs.to_h
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/emery/enum.rb ADDED
@@ -0,0 +1,101 @@
1
+ module Emery
2
+ module Enum
3
+ attr_reader :key, :value
4
+
5
+ def initialize(key, value)
6
+ @key = key
7
+ @value = value
8
+ end
9
+
10
+ def self.included(base)
11
+ base.extend Enumerable
12
+ base.extend ClassMethods
13
+
14
+ base.private_class_method(:new)
15
+ end
16
+
17
+ module ClassMethods
18
+ def check(value)
19
+ T.check_not_nil(self, value)
20
+ if !value?(value)
21
+ raise TypeError.new("Value '#{value.inspect.to_s}' is not a member of enum #{self}")
22
+ end
23
+ end
24
+
25
+ def jsoner_deserialize(json_value)
26
+ T.check(self, json_value)
27
+ end
28
+
29
+ def jsoner_serialize(value)
30
+ T.check(self, value)
31
+ end
32
+
33
+ def define(key, value)
34
+ @_enum_hash ||= {}
35
+ @_enums_by_value ||= {}
36
+
37
+ if @_enum_hash.key?(key) then
38
+ raise TypeError.new("Duplicate key: #{key}")
39
+ end
40
+
41
+ if @_enums_by_value.key?(value) then
42
+ raise TypeError.new("Duplicate value: #{value}")
43
+ end
44
+
45
+ new_instance = new(key, value)
46
+ @_enum_hash[key] = new_instance
47
+ @_enums_by_value[value] = new_instance
48
+
49
+ if key.to_s == key.to_s.upcase
50
+ const_set key, value
51
+ else
52
+ define_singleton_method(key) { value }
53
+ end
54
+ end
55
+
56
+ def each(&block)
57
+ @_enum_hash.each(&block)
58
+ end
59
+
60
+ def parse(k)
61
+ k = k.to_s.upcase
62
+ each do |key, enum|
63
+ return enum.value if key.to_s.upcase == k
64
+ end
65
+ nil
66
+ end
67
+
68
+ def key?(k)
69
+ @_enum_hash.key?(k)
70
+ end
71
+
72
+ def value(k)
73
+ enum = @_enum_hash[k]
74
+ enum.value if enum
75
+ end
76
+
77
+ def value?(v)
78
+ @_enums_by_value.key?(v)
79
+ end
80
+
81
+ def key(v)
82
+ enum = @_enums_by_value[v]
83
+ enum.key if enum
84
+ end
85
+
86
+ def keys
87
+ @_enum_hash.values.map(&:key)
88
+ end
89
+
90
+ def values
91
+ @_enum_hash.values.map(&:value)
92
+ end
93
+
94
+ def to_h
95
+ Hash[@_enum_hash.map do |key, enum|
96
+ [key, enum.value]
97
+ end]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,188 @@
1
+ require "json"
2
+ require "date"
3
+
4
+ require "emery/type"
5
+
6
+ module Emery
7
+ class JsonerError < StandardError
8
+ end
9
+
10
+ module Jsoner
11
+ T::StringFormatted.class_eval do
12
+ def jsoner_deserialize(json_value)
13
+ T.check(self, json_value)
14
+ end
15
+ def jsoner_serialize(value)
16
+ T.check(self, value)
17
+ end
18
+ end
19
+
20
+ T::ArrayType.class_eval do
21
+ def jsoner_deserialize(json_value)
22
+ T.check_not_nil(self, json_value)
23
+ if !json_value.is_a?(Array)
24
+ raise JsonerError.new("JSON value type #{json_value.class} is not Array")
25
+ end
26
+ json_value.map { |item_json_value| Jsoner.deserialize(self.item_type, item_json_value) }
27
+ end
28
+ def jsoner_serialize(value)
29
+ if !value.is_a?(Array)
30
+ raise JsonerError.new("Value type #{json_value.class} is not Array")
31
+ end
32
+ value.map { |item| Jsoner.serialize(self.item_type, item) }
33
+ end
34
+ end
35
+
36
+ T::HashType.class_eval do
37
+ def jsoner_deserialize(json_value)
38
+ T.check_not_nil(self, json_value)
39
+ if self.key_type != String
40
+ raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
41
+ end
42
+ if !json_value.is_a?(Hash)
43
+ raise JsonerError.new("JSON value type #{json_value.class} is not Hash")
44
+ end
45
+ json_value.map do |key, value|
46
+ [T.check(self.key_type, key), Jsoner.deserialize(self.value_type, value)]
47
+ end.to_h
48
+ end
49
+ def jsoner_serialize(value)
50
+ if self.key_type != String
51
+ raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
52
+ end
53
+ if !value.is_a?(Hash)
54
+ raise JsonerError.new("Value type #{value.class} is not Hash")
55
+ end
56
+ value.map do |key, value|
57
+ [T.check(self.key_type, key), Jsoner.serialize(self.value_type, value)]
58
+ end.to_h
59
+ end
60
+ end
61
+
62
+ T::AnyType.class_eval do
63
+ def jsoner_deserialize(json_value)
64
+ types.each do |type|
65
+ begin
66
+ return Jsoner.deserialize(type, json_value)
67
+ rescue TypeError
68
+ end
69
+ end
70
+ raise JsonerError.new("Value '#{json_value.inspect.to_s}' can not be deserialized as any of #{@types.map { |t| t.to_s}.join(', ')}")
71
+ end
72
+ def jsoner_serialize(value)
73
+ T.check(self, value)
74
+ type = types.find {|t| T.instance_of?(t, value) }
75
+ Jsoner.serialize(type, value)
76
+ end
77
+ end
78
+
79
+ T::Nilable.class_eval do
80
+ def jsoner_deserialize(json_value)
81
+ if json_value != nil
82
+ Jsoner.deserialize(self.type, json_value)
83
+ else
84
+ nil
85
+ end
86
+ end
87
+ def jsoner_serialize(value)
88
+ if value != nil
89
+ Jsoner.serialize(self.type, value)
90
+ else
91
+ nil
92
+ end
93
+ end
94
+ end
95
+
96
+ module FloatSerializer
97
+ def self.jsoner_deserialize(json_value)
98
+ T.check(T.any(Float, Integer), json_value)
99
+ json_value.to_f
100
+ end
101
+ def self.jsoner_serialize(value)
102
+ T.check(Float, value)
103
+ end
104
+ end
105
+
106
+ module DateTimeSerializer
107
+ def self.jsoner_deserialize(json_value)
108
+ T.check(String, json_value)
109
+ begin
110
+ DateTime.strptime(json_value, '%Y-%m-%dT%H:%M:%S')
111
+ rescue
112
+ raise JsonerError.new("Failed to parse DateTime from '#{json_value.inspect.to_s}' format %Y-%m-%dT%H:%M:%S is required")
113
+ end
114
+ end
115
+ def self.jsoner_serialize(value)
116
+ T.check(DateTime, value)
117
+ value.strftime('%Y-%m-%dT%H:%M:%S')
118
+ end
119
+ end
120
+
121
+ module DateSerializer
122
+ def self.jsoner_deserialize(json_value)
123
+ T.check(String, json_value)
124
+ begin
125
+ Date.strptime(json_value, '%Y-%m-%d')
126
+ rescue
127
+ raise JsonerError.new("Failed to parse Date from '#{json_value.inspect.to_s}' format %Y-%m-%d is required")
128
+ end
129
+ end
130
+ def self.jsoner_serialize(value)
131
+ T.check(Date, value)
132
+ value.strftime('%Y-%m-%d')
133
+ end
134
+ end
135
+
136
+ @@serializers = {
137
+ Float => FloatSerializer,
138
+ Date => DateSerializer,
139
+ DateTime => DateTimeSerializer
140
+ }
141
+ def self.add_serializer(type, serializer)
142
+ @@serializers[type] = serializer
143
+ end
144
+
145
+ def Jsoner.from_json(type, json)
146
+ data = JSON.parse(json)
147
+ return deserialize(type, data)
148
+ end
149
+
150
+ def Jsoner.deserialize(type, json_value)
151
+ begin
152
+ if type.methods.include? :jsoner_deserialize
153
+ return type.jsoner_deserialize(json_value)
154
+ elsif @@serializers.include? type
155
+ return @@serializers[type].jsoner_deserialize(json_value)
156
+ else
157
+ if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
158
+ raise JsonerError.new("Type #{type} is not supported in Jsoner deserialization")
159
+ end
160
+ return T.check(type, json_value)
161
+ end
162
+ rescue StandardError => error
163
+ raise JsonerError.new(error.message)
164
+ end
165
+ end
166
+
167
+ def Jsoner.to_json(type, value)
168
+ JSON.dump(serialize(type, value))
169
+ end
170
+
171
+ def Jsoner.serialize(type, value)
172
+ begin
173
+ if type.methods.include? :jsoner_serialize
174
+ return type.jsoner_serialize(value)
175
+ elsif @@serializers.include? type
176
+ return @@serializers[type].jsoner_serialize(value)
177
+ else
178
+ if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
179
+ raise JsonerError.new("Type #{type} is not supported in Jsoner serialization")
180
+ end
181
+ return T.check(type, value)
182
+ end
183
+ rescue StandardError => error
184
+ raise JsonerError.new(error.message)
185
+ end
186
+ end
187
+ end
188
+ end
data/lib/emery/tod.rb ADDED
@@ -0,0 +1,262 @@
1
+ =begin
2
+
3
+ source: https://github.com/jackc/tod
4
+
5
+ Copyright (c) 2010-2015 Jack Christensen
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ =end
27
+
28
+ module Emery
29
+ class TimeOfDay
30
+ include Comparable
31
+
32
+ def self.jsoner_deserialize(json_value)
33
+ TimeOfDay.parse(T.check(String, json_value))
34
+ end
35
+ def self.jsoner_serialize(value)
36
+ T.check(TimeOfDay, value).to_s
37
+ end
38
+
39
+ attr_reader :hour, :minute, :second, :second_of_day
40
+ alias_method :min, :minute
41
+ alias_method :sec, :second
42
+ alias_method :to_i, :second_of_day
43
+
44
+ PARSE_24H_REGEX = /
45
+ \A
46
+ ([01]?\d|2[0-4])
47
+ :?
48
+ ([0-5]\d)?
49
+ :?
50
+ ([0-5]\d)?
51
+ \z
52
+ /x
53
+
54
+ PARSE_12H_REGEX = /
55
+ \A
56
+ (0?\d|1[0-2])
57
+ :?
58
+ ([0-5]\d)?
59
+ :?
60
+ ([0-5]\d)?
61
+ \s*
62
+ ([ap])
63
+ \.?
64
+ \s*
65
+ m?
66
+ \.?
67
+ \z
68
+ /x
69
+
70
+ WORDS = {
71
+ "noon" => "12pm".freeze,
72
+ "midnight" => "12am".freeze
73
+ }
74
+
75
+ NUM_SECONDS_IN_DAY = 86400
76
+ NUM_SECONDS_IN_HOUR = 3600
77
+ NUM_SECONDS_IN_MINUTE = 60
78
+
79
+ FORMATS = {
80
+ short: "%-l:%M %P".freeze,
81
+ medium: "%-l:%M:%S %P".freeze,
82
+ time: "%H:%M".freeze
83
+ }
84
+
85
+ def initialize(h, m=0, s=0)
86
+ @hour = Integer(h)
87
+ @minute = Integer(m)
88
+ @second = Integer(s)
89
+
90
+ raise ArgumentError, "hour must be between 0 and 24" unless (0..24).include?(@hour)
91
+ if @hour == 24 && (@minute != 0 || @second != 0)
92
+ raise ArgumentError, "hour can only be 24 when minute and second are 0"
93
+ end
94
+ raise ArgumentError, "minute must be between 0 and 59" unless (0..59).include?(@minute)
95
+ raise ArgumentError, "second must be between 0 and 59" unless (0..59).include?(@second)
96
+
97
+ @second_of_day = @hour * 60 * 60 + @minute * 60 + @second
98
+
99
+ freeze # TimeOfDay instances are value objects
100
+ end
101
+
102
+ def <=>(other)
103
+ return unless other.respond_to?(:second_of_day)
104
+ @second_of_day <=> other.second_of_day
105
+ end
106
+
107
+ # Rounding to the given nearest number of seconds
108
+ def round(round_sec = 1)
109
+ down = self - (self.to_i % round_sec)
110
+ up = down + round_sec
111
+
112
+ difference_down = self - down
113
+ difference_up = up - self
114
+
115
+ if (difference_down < difference_up)
116
+ return down
117
+ else
118
+ return up
119
+ end
120
+ end
121
+
122
+ # Formats identically to Time#strftime
123
+ def strftime(format_string)
124
+ # Special case 2400 because strftime will load TimeOfDay into Time which
125
+ # will convert 24 to 0
126
+ format_string = format_string.gsub(/%H|%k/, '24') if @hour == 24
127
+ Time.local(2000,1,1, @hour, @minute, @second).strftime(format_string)
128
+ end
129
+
130
+ def to_formatted_s(format = :default)
131
+ if formatter = FORMATS[format]
132
+ if formatter.respond_to?(:call)
133
+ formatter.call(self).to_s
134
+ else
135
+ strftime(formatter)
136
+ end
137
+ else
138
+ strftime "%H:%M:%S"
139
+ end
140
+ end
141
+ alias_method :to_s, :to_formatted_s
142
+
143
+ def value_for_database
144
+ to_s
145
+ end
146
+
147
+ # Return a new TimeOfDay num_seconds greater than self. It will wrap around
148
+ # at midnight.
149
+ def +(num_seconds)
150
+ TimeOfDay.from_second_of_day @second_of_day + num_seconds
151
+ end
152
+
153
+ # Return a new TimeOfDay num_seconds less than self. It will wrap around
154
+ # at midnight.
155
+ def -(other)
156
+ if other.instance_of?(TimeOfDay)
157
+ TimeOfDay.from_second_of_day @second_of_day - other.second_of_day
158
+ else
159
+ TimeOfDay.from_second_of_day @second_of_day - other
160
+ end
161
+ end
162
+
163
+ # Returns a Time instance on date using self as the time of day
164
+ # Optional time_zone will build time in that zone
165
+ def on(date, time_zone=Tod::TimeOfDay.time_zone)
166
+ time_zone.local date.year, date.month, date.day, @hour, @minute, @second
167
+ end
168
+
169
+ # Build a new TimeOfDay instance from second_of_day
170
+ #
171
+ # TimeOfDay.from_second_of_day(3600) == TimeOfDay.new(1) # => true
172
+ def self.from_second_of_day(second_of_day)
173
+ second_of_day = Integer(second_of_day)
174
+ return new 24 if second_of_day == NUM_SECONDS_IN_DAY
175
+ remaining_seconds = second_of_day % NUM_SECONDS_IN_DAY
176
+ hour = remaining_seconds / NUM_SECONDS_IN_HOUR
177
+ remaining_seconds -= hour * NUM_SECONDS_IN_HOUR
178
+ minute = remaining_seconds / NUM_SECONDS_IN_MINUTE
179
+ remaining_seconds -= minute * NUM_SECONDS_IN_MINUTE
180
+ new hour, minute, remaining_seconds
181
+ end
182
+ class << self
183
+ alias :from_i :from_second_of_day
184
+ end
185
+
186
+ # Build a TimeOfDay instance from string
187
+ #
188
+ # Strings only need to contain an hour. Minutes, seconds, AM or PM, and colons
189
+ # are all optional.
190
+ # TimeOfDay.parse "8" # => 08:00:00
191
+ # TimeOfDay.parse "8am" # => 08:00:00
192
+ # TimeOfDay.parse "8pm" # => 20:00:00
193
+ # TimeOfDay.parse "8p" # => 20:00:00
194
+ # TimeOfDay.parse "9:30" # => 09:30:00
195
+ # TimeOfDay.parse "15:30" # => 15:30:00
196
+ # TimeOfDay.parse "3:30pm" # => 15:30:00
197
+ # TimeOfDay.parse "1230" # => 12:30:00
198
+ # TimeOfDay.parse "3:25:58" # => 03:25:58
199
+ # TimeOfDay.parse "515p" # => 17:15:00
200
+ # TimeOfDay.parse "151253" # => 15:12:53
201
+ # You can give a block, that is called with the input if the string is not parsable.
202
+ # If no block is given an ArgumentError is raised if try_parse returns nil.
203
+ def self.parse(tod_string)
204
+ try_parse(tod_string) || (block_given? ? yield(tod_string) : raise(ArgumentError, "Invalid time of day string"))
205
+ end
206
+
207
+ # Same as parse(), but return nil if not parsable (instead of raising an error)
208
+ # TimeOfDay.try_parse "8am" # => 08:00:00
209
+ # TimeOfDay.try_parse "" # => nil
210
+ # TimeOfDay.try_parse "abc" # => nil
211
+ def self.try_parse(tod_string)
212
+ tod_string = tod_string.to_s
213
+ tod_string = tod_string.strip
214
+ tod_string = tod_string.downcase
215
+ tod_string = WORDS[tod_string] || tod_string
216
+ if PARSE_24H_REGEX =~ tod_string || PARSE_12H_REGEX =~ tod_string
217
+ hour, minute, second, a_or_p = $1.to_i, $2.to_i, $3.to_i, $4
218
+ if hour == 12 && a_or_p == "a"
219
+ hour = 0
220
+ elsif hour < 12 && a_or_p == "p"
221
+ hour += 12
222
+ end
223
+
224
+ new hour, minute, second
225
+ else
226
+ nil
227
+ end
228
+ end
229
+
230
+ # Determine if a string is parsable into a TimeOfDay instance
231
+ # TimeOfDay.parsable? "8am" # => true
232
+ # TimeOfDay.parsable? "abc" # => false
233
+ def self.parsable?(tod_string)
234
+ !!try_parse(tod_string)
235
+ end
236
+
237
+ # If ActiveSupport TimeZone is available and set use current time zone else return Time
238
+ def self.time_zone
239
+ (Time.respond_to?(:zone) && Time.zone) || Time
240
+ end
241
+
242
+ def self.dump(time_of_day)
243
+ time_of_day =
244
+ if time_of_day.is_a? Hash
245
+ # rails multiparam attribute
246
+ # get hour, minute and second and construct new TimeOfDay object
247
+ ::Tod::TimeOfDay.new(time_of_day[4], time_of_day[5], time_of_day[6])
248
+ else
249
+ # return nil, if input is not parsable
250
+ Tod::TimeOfDay(time_of_day){}
251
+ end
252
+ time_of_day.to_s if time_of_day
253
+ end
254
+
255
+ def self.load(time)
256
+ if time && !time.to_s.empty?
257
+ return ::Tod::TimeOfDay.new(24) if time.respond_to?(:day) && time.day == 2 && time.hour == 0 && time.min == 0 && time.sec == 0
258
+ ::Tod::TimeOfDay(time)
259
+ end
260
+ end
261
+ end
262
+ end