schemad 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +327 -0
- data/Rakefile +8 -0
- data/lib/schemad/abstract_handler.rb +16 -0
- data/lib/schemad/entity.rb +74 -0
- data/lib/schemad/extensions.rb +39 -0
- data/lib/schemad/normalizer.rb +93 -0
- data/lib/schemad/type_handler.rb +34 -0
- data/lib/schemad/types/boolean_handler.rb +13 -0
- data/lib/schemad/types/integer_handler.rb +16 -0
- data/lib/schemad/types/string_handler.rb +11 -0
- data/lib/schemad/types/time_handler.rb +19 -0
- data/lib/schemad/version.rb +3 -0
- data/lib/schemad.rb +7 -0
- data/schemad.gemspec +29 -0
- data/spec/entity_spec.rb +56 -0
- data/spec/extensions_spec.rb +41 -0
- data/spec/fixtures/demo_class.rb +8 -0
- data/spec/normalizer_spec.rb +110 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/type_handler_spec.rb +125 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 42533ea7f5721acf5f2bcc3c10a10322b61d6e76
|
4
|
+
data.tar.gz: d24a50de59c6ce8d81d5c5466f19b993e05bfe06
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9cced2f7ca522ac55286b5c34e7ab1539a342c16c97831cbeb838987f1885790dd81d2bc73a80f4783d166ef5bbc0258a5fc61cf8fab9c97befe6c812dd9f1a6
|
7
|
+
data.tar.gz: c828d9d9c9c0ab5e7df1d50613ec45c63f7d13f91ac5cb7db404a896f2158b63a9270580ba3b41707c3a46020ae586d898c53e462a3b69a97db3a3c02b3a2998
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Luke van der Hoeven
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
# Schemad
|
2
|
+
|
3
|
+
Schemad is a simple metagem to aid integrating legacy or third-party datasets into other projects. It's especially geared towards unifying multiple datasets into consistent data structures for ease of and consistency in use.
|
4
|
+
|
5
|
+
This gem has two main parts: Normalizers and Entities.
|
6
|
+
|
7
|
+
## Normalizers
|
8
|
+
|
9
|
+
Normalizers are the translators between different datasets. They take misshaped data and help mold it into a consistent form before turning them into objects for general use.
|
10
|
+
|
11
|
+
For example, let's say I want to pull commit data from [GitHub](https://github.com) and [BitBucket](https://bitbucket.org) and do something with the two datasets. Let's look at the API for both and the kind of data they return for a commit object.
|
12
|
+
|
13
|
+
### GitHub Commit API
|
14
|
+
> [Source](https://developer.github.com/v3/git/commits/)
|
15
|
+
|
16
|
+
```json
|
17
|
+
{
|
18
|
+
"sha": "7638417db6d59f3c431d3e1f261cc637155684cd",
|
19
|
+
"url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd",
|
20
|
+
"author": {
|
21
|
+
"date": "2010-04-10T14:10:01-07:00",
|
22
|
+
"name": "Scott Chacon",
|
23
|
+
"email": "schacon@gmail.com"
|
24
|
+
},
|
25
|
+
"committer": {
|
26
|
+
"date": "2010-04-10T14:10:01-07:00",
|
27
|
+
"name": "Scott Chacon",
|
28
|
+
"email": "schacon@gmail.com"
|
29
|
+
},
|
30
|
+
"message": "added readme, because im a good github citizen\n",
|
31
|
+
"tree": {
|
32
|
+
"url": "https://api.github.com/repos/octocat/Hello-World/git/trees/691272480426f78a0138979dd3ce63b77f706feb",
|
33
|
+
"sha": "691272480426f78a0138979dd3ce63b77f706feb"
|
34
|
+
},
|
35
|
+
"parents": [
|
36
|
+
{
|
37
|
+
"url": "https://api.github.com/repos/octocat/Hello-World/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5",
|
38
|
+
"sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5"
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
44
|
+
### BitBucket Commit API
|
45
|
+
> [Source](https://confluence.atlassian.com/display/BITBUCKET/commits+or+commit+Resource#commitsorcommitResource-GETanindividualcommit)
|
46
|
+
|
47
|
+
```json
|
48
|
+
{
|
49
|
+
hash: "61d9e64348f9da407e62f64726337fd3bb24b466",
|
50
|
+
links: {
|
51
|
+
self: {
|
52
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/commit/61d9e64348f9da407e62f64726337fd3bb24b466"
|
53
|
+
},
|
54
|
+
comments: {
|
55
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/commit/61d9e64348f9da407e62f64726337fd3bb24b466/comments"
|
56
|
+
},
|
57
|
+
patch: {
|
58
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/patch/61d9e64348f9da407e62f64726337fd3bb24b466"
|
59
|
+
},
|
60
|
+
html: {
|
61
|
+
href: "https://api.bitbucket.org/atlassian/atlassian-rest/commits/61d9e64348f9da407e62f64726337fd3bb24b466"
|
62
|
+
},
|
63
|
+
diff: {
|
64
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/diff/61d9e64348f9da407e62f64726337fd3bb24b466"
|
65
|
+
},
|
66
|
+
approve: {
|
67
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/commit/61d9e64348f9da407e62f64726337fd3bb24b466/approve"
|
68
|
+
}
|
69
|
+
},
|
70
|
+
repository: {
|
71
|
+
links: {
|
72
|
+
self: {
|
73
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest"
|
74
|
+
},
|
75
|
+
avatar: {
|
76
|
+
href: "https://d3oaxc4q5k2d6q.cloudfront.net/m/bf1e763db20f/img/language-avatars/java_16.png"
|
77
|
+
}
|
78
|
+
},
|
79
|
+
full_name: "atlassian/atlassian-rest",
|
80
|
+
name: "atlassian-rest"
|
81
|
+
},
|
82
|
+
author: {
|
83
|
+
raw: "Joseph Walton <jwalton@atlassian.com>",
|
84
|
+
user: {
|
85
|
+
username: "jwalton",
|
86
|
+
display_name: "Joseph Walton",
|
87
|
+
links: {
|
88
|
+
self: {
|
89
|
+
href: "https://api.bitbucket.org/2.0/users/jwalton"
|
90
|
+
},
|
91
|
+
avatar: {
|
92
|
+
href: "https://secure.gravatar.com/avatar/8e6e91101e3ed8a332dbebfdf59a3cef?d=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2Fbf1e763db20f%2Fimg%2Fdefault_avatar%2F32%2Fuser_blue.png&s=32"
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
},
|
97
|
+
participants: [{
|
98
|
+
role: "PARTICIPANT",
|
99
|
+
user: {
|
100
|
+
username: "evzijst",
|
101
|
+
display_name: "Erik van Zijst",
|
102
|
+
links: {
|
103
|
+
self: {
|
104
|
+
href: "https://api.bitbucket.org/2.0/users/evzijst"
|
105
|
+
},
|
106
|
+
avatar: {
|
107
|
+
href: "https://secure.gravatar.com/avatar/f6bcbb4e3f665e74455bd8c0b4b3afba?d=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2Fbf1e763db20f%2Fimg%2Fdefault_avatar%2F32%2Fuser_blue.png&s=32"
|
108
|
+
}
|
109
|
+
}
|
110
|
+
},
|
111
|
+
approved: false
|
112
|
+
}],
|
113
|
+
parents: [{
|
114
|
+
hash: "59721f593b020123a75424285845325126f56e2e",
|
115
|
+
links: {
|
116
|
+
self: {
|
117
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/commit/59721f593b020123a75424285845325126f56e2e"
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}, {
|
121
|
+
hash: "56c49d8b2ae3a094fa7ba5a1251d6dd2c7c66993",
|
122
|
+
links: {
|
123
|
+
self: {
|
124
|
+
href: "https://api.bitbucket.org/2.0/repositories/atlassian/atlassian-rest/commit/56c49d8b2ae3a094fa7ba5a1251d6dd2c7c66993"
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}],
|
128
|
+
date: "2013-10-21T07:21:51+00:00",
|
129
|
+
message: "Merge remote-tracking branch 'origin/rest-2.8.x' "
|
130
|
+
}
|
131
|
+
```
|
132
|
+
|
133
|
+
Obviously, mining the two datasets for a bunch of commits will require a lot of parsing to unify the two data structures presented here.
|
134
|
+
|
135
|
+
In step the Normalizers. For this example ee'd create two separate normalizers for these datasets, one for GH, one for BB:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
class GitHubNormalizer < Schemad::Normalizer
|
139
|
+
normalize :id, key: :sha
|
140
|
+
normalize :url, key: :url
|
141
|
+
normalize :committer, key: "committer/name"
|
142
|
+
normalize :created_date, key: "committer/date"
|
143
|
+
normalize :comment, key: :message
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
We could obviously also include additional data if we wanted. Notice you can use either symbols or strings as keys. If you want to do a deep traversal (more than one level), you will need to use strings with a "/" delimited path.
|
148
|
+
|
149
|
+
Now for BitBucket:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class BitBucketNormalizer < Schemad::Normalizer
|
153
|
+
normalize :id, key: :hash
|
154
|
+
normalize :url, key: "links/self/href"
|
155
|
+
normalize :committer, key: "author/user/display_name"
|
156
|
+
normalize :created_date, key: :date
|
157
|
+
normalize :comment, key: :message
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
Sweet. So now when we get the json from the API, all we need to do with these classes is:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
raw_json = some_http_get("https://api.github.com/path/to/my/commit")
|
165
|
+
github_data = JSON.parse(raw_json)
|
166
|
+
|
167
|
+
parsed = GitHubNormalizer.new.normalize(github_data)
|
168
|
+
```
|
169
|
+
|
170
|
+
And we should then have a plain hash much like the following:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
{
|
174
|
+
id: "7638417db6d59f3c431d3e1f261cc637155684cd",
|
175
|
+
url: "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd",
|
176
|
+
committer: "Scott Chacon",
|
177
|
+
created_date: "2010-04-10T14:10:01-07:00",
|
178
|
+
comment: "added readme, because im a good github citizen\n"
|
179
|
+
}
|
180
|
+
```
|
181
|
+
|
182
|
+
Our BitBucket normalizer would work the same way, we'd just use it to run through data harvested by our BitBucket requests.
|
183
|
+
|
184
|
+
Two things of mention: First, the normalizers can also be passed a block to perform additional data manipulation. For example, assume we wanted to harvest an email field. GitHub provides this to us directly to use:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class GitHubNormalizer < Schemad::Normalizer
|
188
|
+
# ... other normalizers
|
189
|
+
normalize :email, key: "committer/email"
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
However, BitBucket does not directly. It's wrapped within the "raw" field in the author hash. So we can provide additional modifiers to get this:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class BitBucketNormalizer < Schemad::Normalizer
|
197
|
+
# ... other normalizers
|
198
|
+
normalize :email, key: "author/raw" do |value|
|
199
|
+
value.match(/\A[\w|\s]+<(.+)>\z/).captures.first
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
Now the normalizer will use the raw field and pick out the email using a regex matcher. Now you might be tempted to use the normalizer blocks to manipulate the fields into ruby types, as the normalizers do not attempt to parse the data types. You'll notice in the above examples that date strings are left as strings through the normalization. This brings us to our second thing to note: Normalizers _only parse data into consistent structures_. They are not responsible for type casting. This is intentional and brings us to the role of the Entity.
|
205
|
+
|
206
|
+
## Entities
|
207
|
+
|
208
|
+
Entities provide consistent [value objects](http://martinfowler.com/bliki/ValueObject.html) that allow for easily transporting the data to functionality that uses the data. Entities are very limited in functionality and are mainly meant to provide a more ruby-ish means of accessing the data. We _could_ pass around the normalized hashes, but typically, we rubyists like having method access to our data:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
commit.comment # "added readme, because im a good github citizen\n"
|
212
|
+
commit.id # "7638417db6d59f3c431d3e1f261cc637155684cd"
|
213
|
+
comment.created_date # A time object!
|
214
|
+
```
|
215
|
+
|
216
|
+
So this is what Entities provide.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class Commit < Schemad::Entity
|
220
|
+
attribute :id
|
221
|
+
attribute :committer
|
222
|
+
attribute :comment
|
223
|
+
attribute :created_date, type: :date_time
|
224
|
+
attribute :email
|
225
|
+
attribute :url
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
Note that the default attribute type (if not provided) is a string (`:string`). Currently supported types are
|
230
|
+
|
231
|
+
- :string
|
232
|
+
- :time, :date, :date_time (all the same in our case)
|
233
|
+
- :integer
|
234
|
+
- :boolean
|
235
|
+
|
236
|
+
New types are easy to create, more on this in a moment.
|
237
|
+
|
238
|
+
To instantiate these new class, we use the `from_data` method to ensure parsing with the output from the normalizer step above:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
raw_json = some_http_get("https://api.github.com/path/to/my/commit")
|
242
|
+
github_data = JSON.parse(raw_json)
|
243
|
+
|
244
|
+
parsed = GitHubNormalizer.new.normalize(github_data)
|
245
|
+
|
246
|
+
commit = Commit.from_data(parsed)
|
247
|
+
|
248
|
+
commit.comment # "added readme, because im a good github citizen\n"
|
249
|
+
commit.id # "7638417db6d59f3c431d3e1f261cc637155684cd"
|
250
|
+
comment.created_date # A time object!
|
251
|
+
```
|
252
|
+
|
253
|
+
You don't have to use the normalizers to use the `from_data` method. It can be any consistently formatted hash. The keys **must** be accessible by symbol however (use a hash with all symbols as keys or an ActiveSupport/Hashie/other [HashWithIndifferentAccess](http://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html) implementation).
|
254
|
+
|
255
|
+
In fact, both normalizer and entity can be used independent of one another if one or the other isn't required for your use. Just include the library you want:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
require 'schemad/type_handler'
|
259
|
+
require 'schemad/normalizer'
|
260
|
+
require 'schemad/entity'
|
261
|
+
|
262
|
+
# or to get all...
|
263
|
+
require 'schemad'
|
264
|
+
```
|
265
|
+
|
266
|
+
## Type Handlers
|
267
|
+
|
268
|
+
On a side note, we have a number of very simple type handlers, you can see them all in the various type definitions [type_handler](https://github.com/plukevdh/schemad/tree/master/lib/schemad/types).
|
269
|
+
|
270
|
+
It is _also_ possible to use these however you want in your own classes, but there are far more complete and complex type handlers elsewhere. If you find these types unsatisfactory or wish to use additional types, you can easily define your own.
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class YouMomHandler < Schemad::AbstractHandler
|
274
|
+
handle :your_mom
|
275
|
+
|
276
|
+
def parse(value)
|
277
|
+
"Your Mom"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# register this handler
|
282
|
+
Schemad::TypeHandler.register YourMomHandler
|
283
|
+
|
284
|
+
# alternatively...
|
285
|
+
YourMomHandler.register_with Schemad::TypeHandler
|
286
|
+
```
|
287
|
+
|
288
|
+
Now when you want to have your entity turn anything into your mother, simply add the type `:your_mom` to the attribute definition.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
class Commit < Schemad::Entity
|
292
|
+
# ...
|
293
|
+
attribute :comment, type: :your_mom
|
294
|
+
end
|
295
|
+
|
296
|
+
commit = Commit.from_data(parsed)
|
297
|
+
|
298
|
+
commit.comment # "Your Mom"
|
299
|
+
```
|
300
|
+
|
301
|
+
## Notes
|
302
|
+
|
303
|
+
[Hashie](https://github.com/intridea/hashie) is probably a better idea than this gem. But I couldn't find a decent way to combine the [DeepFetch](https://github.com/intridea/hashie#deepfetch) functionality with the [Dash](https://github.com/intridea/hashie#dash)/[Trash](https://github.com/intridea/hashie#trash) functionality. This is also likely much ligher weight and therefore about half as meta.
|
304
|
+
|
305
|
+
Be warned, this is a lot of crazy metacode and it's _mostly_ recommended you don't use this for real. Mostly. But it is awesome.
|
306
|
+
|
307
|
+
## Installation
|
308
|
+
|
309
|
+
Add this line to your application's Gemfile:
|
310
|
+
|
311
|
+
gem 'schemad'
|
312
|
+
|
313
|
+
And then execute:
|
314
|
+
|
315
|
+
$ bundle
|
316
|
+
|
317
|
+
Or install it yourself as:
|
318
|
+
|
319
|
+
$ gem install schemad
|
320
|
+
|
321
|
+
## Contributing
|
322
|
+
|
323
|
+
1. Fork it ( https://github.com/[my-github-username]/schemad/fork )
|
324
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
325
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
326
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
327
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'schemad/extensions'
|
2
|
+
require 'schemad/type_handler'
|
3
|
+
|
4
|
+
module Schemad
|
5
|
+
class Entity
|
6
|
+
extend Schemad::Extensions
|
7
|
+
|
8
|
+
def self.inherited(subclass)
|
9
|
+
subclass.instance_variable_set(:@attributes, [])
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.attribute(name, args={}, &block)
|
13
|
+
attr_accessor name
|
14
|
+
|
15
|
+
define_parser_for(name, args, &block)
|
16
|
+
|
17
|
+
@attributes << name
|
18
|
+
end
|
19
|
+
|
20
|
+
# expect data hash to have symbol keys at this point
|
21
|
+
# normalizer should standardize this
|
22
|
+
def self.from_data(data)
|
23
|
+
obj = new
|
24
|
+
|
25
|
+
@attributes.each do |key|
|
26
|
+
value = obj.send "parse_#{key}", data
|
27
|
+
obj.send "#{key}=", value
|
28
|
+
end
|
29
|
+
|
30
|
+
obj
|
31
|
+
end
|
32
|
+
|
33
|
+
def attribute_names
|
34
|
+
self.class.instance_variable_get(:@attributes)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
hash = {}
|
39
|
+
attribute_names.each do |key|
|
40
|
+
hash[key] = send key
|
41
|
+
end
|
42
|
+
|
43
|
+
hash
|
44
|
+
end
|
45
|
+
alias_method :attributes, :to_hash
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def self.define_parser_for(name, args, &block)
|
50
|
+
define_method "parse_#{name}" do |data|
|
51
|
+
value = data[name]
|
52
|
+
value ||= get_default(args[:default])
|
53
|
+
|
54
|
+
self.send "#{name}=", coerce_to_type(value, args[:type])
|
55
|
+
end
|
56
|
+
|
57
|
+
define_method "#{name}?" do
|
58
|
+
!!send(name)
|
59
|
+
end if args[:type] == :boolean
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_default(default_provider)
|
63
|
+
return default_provider unless default_provider.is_a? Proc
|
64
|
+
default_provider.call
|
65
|
+
end
|
66
|
+
|
67
|
+
def coerce_to_type(value, type)
|
68
|
+
type ||= :string
|
69
|
+
|
70
|
+
handler = TypeHandler.new type
|
71
|
+
handler.parse(value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'active_support/hash_with_indifferent_access'
|
5
|
+
|
6
|
+
module Schemad
|
7
|
+
module Extensions
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def base_class_name
|
12
|
+
name.demodulize
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def base_class_name
|
17
|
+
self.class.base_class_name
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
# TODO: decide if I still want this method around. modifies repos
|
22
|
+
# def add_behavior(repo, additions)
|
23
|
+
# repo.extend additions
|
24
|
+
# end
|
25
|
+
|
26
|
+
def constantize(string)
|
27
|
+
string.to_s.constantize
|
28
|
+
end
|
29
|
+
|
30
|
+
def classify(string)
|
31
|
+
string = "nil" if string.nil?
|
32
|
+
string.to_s.classify
|
33
|
+
end
|
34
|
+
|
35
|
+
def indifferent_hash(hash)
|
36
|
+
ActiveSupport::HashWithIndifferentAccess.new hash
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'schemad/extensions'
|
2
|
+
|
3
|
+
module Schemad
|
4
|
+
class Normalizer
|
5
|
+
include Schemad::Extensions
|
6
|
+
|
7
|
+
DELIMITER = "/"
|
8
|
+
|
9
|
+
InvalidPath = Class.new(Exception)
|
10
|
+
|
11
|
+
def self.inherited(subclass)
|
12
|
+
subclass.instance_variable_set(:@normalizers, {})
|
13
|
+
subclass.instance_variable_set(:@allowed_attributes, [])
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.include_fields(fields)
|
17
|
+
@allowed_attributes.concat fields
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.normalize(name, args={}, &block)
|
21
|
+
lookup = args[:key] || name
|
22
|
+
method_name = normalizer_method_name(name)
|
23
|
+
|
24
|
+
@normalizers[lookup] = name
|
25
|
+
@allowed_attributes << lookup
|
26
|
+
|
27
|
+
define_method method_name do |data|
|
28
|
+
value = find_value lookup, data
|
29
|
+
return value unless block_given?
|
30
|
+
|
31
|
+
yield value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def normalize(data)
|
36
|
+
normalized = {}
|
37
|
+
|
38
|
+
allowed_attributes.each do |key|
|
39
|
+
to_key = normalizers[key]
|
40
|
+
|
41
|
+
if to_key
|
42
|
+
normalized[to_key] = self.send normalizer_method_name(to_key), data
|
43
|
+
else
|
44
|
+
normalized[key_from_path(key)] = find_value(key, data)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
normalized
|
49
|
+
end
|
50
|
+
|
51
|
+
def normalizers
|
52
|
+
self.class.instance_variable_get(:@normalizers)
|
53
|
+
end
|
54
|
+
|
55
|
+
def allowed_attributes
|
56
|
+
self.class.instance_variable_get(:@allowed_attributes)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def path_steps(key)
|
61
|
+
key.to_s.split(DELIMITER)
|
62
|
+
end
|
63
|
+
|
64
|
+
def key_from_path(path)
|
65
|
+
path_steps(path).last.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_value(key, data)
|
69
|
+
begin
|
70
|
+
search_data path_steps(key), indifferent_hash(data)
|
71
|
+
rescue InvalidPath => e
|
72
|
+
# rethrow with more info
|
73
|
+
raise e, "Can't find value for \"#{key}\""
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def search_data(steps, data)
|
78
|
+
step = steps.shift
|
79
|
+
return data unless step
|
80
|
+
raise InvalidPath if data.nil?
|
81
|
+
|
82
|
+
search_data steps, data[step]
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.normalizer_method_name(field)
|
86
|
+
"normalize_#{field}".to_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
def normalizer_method_name(field)
|
90
|
+
self.class.normalizer_method_name(field)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'schemad/extensions'
|
3
|
+
|
4
|
+
module Schemad
|
5
|
+
class TypeHandler
|
6
|
+
include Extensions
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
UnknownHandler = Class.new(Exception)
|
10
|
+
|
11
|
+
def self.register(handler)
|
12
|
+
@types ||= {}
|
13
|
+
|
14
|
+
handler.handles.each do |type|
|
15
|
+
@types[type] = handler
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(type=:string)
|
20
|
+
handler = handlers[type]
|
21
|
+
|
22
|
+
raise UnknownHandler, "No known handlers for #{classify(type)}" if handler.nil?
|
23
|
+
|
24
|
+
@handler = handler.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def_delegators :@handler, :parse
|
28
|
+
|
29
|
+
private
|
30
|
+
def handlers
|
31
|
+
self.class.instance_variable_get(:@types) || {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Schemad
|
2
|
+
class BooleanHandler < AbstractHandler
|
3
|
+
VALID_TRUTHS = ["true", true, "t", "T", "1", 1, "TRUE"]
|
4
|
+
|
5
|
+
handle :boolean, :bool
|
6
|
+
|
7
|
+
def parse(value)
|
8
|
+
VALID_TRUTHS.include? value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Schemad::BooleanHandler.register_with Schemad::TypeHandler
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Schemad
|
2
|
+
class IntegerHandler < AbstractHandler
|
3
|
+
handle :integer
|
4
|
+
|
5
|
+
def parse(value)
|
6
|
+
case value
|
7
|
+
when TrueClass, FalseClass
|
8
|
+
value ? 1 : 0
|
9
|
+
else
|
10
|
+
value.to_i rescue nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Schemad::IntegerHandler.register_with Schemad::TypeHandler
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Schemad
|
2
|
+
class TimeHandler < AbstractHandler
|
3
|
+
handle :time, :date, :date_time
|
4
|
+
|
5
|
+
def parse(value)
|
6
|
+
return value.to_time if value.respond_to?(:to_time)
|
7
|
+
|
8
|
+
begin
|
9
|
+
Time.at(value)
|
10
|
+
rescue TypeError => e
|
11
|
+
Time.parse(value)
|
12
|
+
rescue ArgumentError => e
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Schemad::TimeHandler.register_with Schemad::TypeHandler
|
data/lib/schemad.rb
ADDED
data/schemad.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'schemad/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "schemad"
|
8
|
+
spec.version = Schemad::VERSION
|
9
|
+
spec.authors = ["Luke van der Hoeven"]
|
10
|
+
spec.email = ["hungerandthirst@gmail.com"]
|
11
|
+
spec.summary = %q{Simple schema DSL for services}
|
12
|
+
spec.description = %q{
|
13
|
+
This gem allows easy attribute definition, type casting and special handling
|
14
|
+
of data returned from a service of some kind. It's meant to be incredibly general
|
15
|
+
purpose. Maybe this is a bad idea...
|
16
|
+
}
|
17
|
+
spec.homepage = ""
|
18
|
+
spec.license = "MIT"
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0")
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "activesupport", "~> 4.1"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
end
|
data/spec/entity_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'timecop'
|
3
|
+
require 'schemad/entity'
|
4
|
+
|
5
|
+
require_relative 'fixtures/demo_class'
|
6
|
+
|
7
|
+
describe Schemad::Entity do
|
8
|
+
before { Timecop.freeze }
|
9
|
+
after { Timecop.return }
|
10
|
+
|
11
|
+
Given(:normal_data) {{
|
12
|
+
world: "coordinates",
|
13
|
+
cool: true,
|
14
|
+
roads: 5,
|
15
|
+
beasts: "1337"
|
16
|
+
}}
|
17
|
+
|
18
|
+
context "#from_data" do
|
19
|
+
Given(:ent) { Ent.from_data(normal_data) }
|
20
|
+
|
21
|
+
Then { ent.attribute_names.should == [:forest, :roads, :beasts, :world, :cool, :created]}
|
22
|
+
|
23
|
+
context "defaults or nil get used when no data" do
|
24
|
+
|
25
|
+
Then { ent.forest.should == "Green" }
|
26
|
+
And { ent.cool.should be_true }
|
27
|
+
And { ent.created.should eq(Time.now) }
|
28
|
+
And { ent.roads.should == 5 }
|
29
|
+
And { ent.world.should == "coordinates" }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "parses types" do
|
33
|
+
Then { ent.beasts.should == 1337 }
|
34
|
+
end
|
35
|
+
|
36
|
+
context "defines #? method for bools" do
|
37
|
+
Then { ent.should be_cool }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "assumes string if no type given" do
|
41
|
+
Then { ent.world.should be_a(String) }
|
42
|
+
end
|
43
|
+
|
44
|
+
context "can get all params as a hash" do
|
45
|
+
When(:hash) { ent.to_hash }
|
46
|
+
Then { hash.should == {
|
47
|
+
forest: "Green",
|
48
|
+
roads: 5,
|
49
|
+
beasts: 1337,
|
50
|
+
world: "coordinates",
|
51
|
+
cool: true,
|
52
|
+
created: Time.now
|
53
|
+
}}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schemad/extensions'
|
3
|
+
|
4
|
+
class Demo
|
5
|
+
class Foo
|
6
|
+
class Bar
|
7
|
+
include Schemad::Extensions
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Schemad::Extensions do
|
13
|
+
Given(:ext) { Demo::Foo::Bar.new }
|
14
|
+
|
15
|
+
context "base class name getter" do
|
16
|
+
When(:class_base) { Demo::Foo::Bar.base_class_name }
|
17
|
+
Then { class_base.should == "Bar" }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "instance method for base class" do
|
21
|
+
When(:base) { ext.base_class_name }
|
22
|
+
Then { base.should == "Bar" }
|
23
|
+
end
|
24
|
+
|
25
|
+
context "constantizer" do
|
26
|
+
When(:const) { ext.send :constantize, "Demo::Foo::Bar" }
|
27
|
+
Then { const.should == Demo::Foo::Bar }
|
28
|
+
end
|
29
|
+
|
30
|
+
context "classifier" do
|
31
|
+
When(:classy) { ext.send :classify, "demo_foo_bar" }
|
32
|
+
Then { classy.should == "DemoFooBar" }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "indifferent hash wrapper" do
|
36
|
+
When(:converted) { ext.send :indifferent_hash, {one: 1, "two" => 2, "Three and Four" => 34} }
|
37
|
+
Then { converted["one"].should == 1 }
|
38
|
+
And { converted[:two].should == 2 }
|
39
|
+
And { converted["Three and Four"].should == 34 }
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class Ent < Schemad::Entity
|
2
|
+
attribute :forest, type: :string, default: "Green"
|
3
|
+
attribute :roads, type: :integer
|
4
|
+
attribute :beasts, type: :integer
|
5
|
+
attribute :world
|
6
|
+
attribute :cool, type: :boolean
|
7
|
+
attribute :created, type: :date_time, default: -> { Time.now }
|
8
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'schemad/normalizer'
|
3
|
+
|
4
|
+
class Demalizer < Schemad::Normalizer
|
5
|
+
normalize :world, key: "Middle Earth" do |val|
|
6
|
+
val.upcase
|
7
|
+
end
|
8
|
+
normalize :roads do |val|
|
9
|
+
val * 10
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Schemad::Normalizer do
|
14
|
+
Given(:data) {{
|
15
|
+
"Middle Earth" => "coordinates",
|
16
|
+
cool: "true",
|
17
|
+
roads: 5,
|
18
|
+
"beasts" => "1337"
|
19
|
+
}}
|
20
|
+
|
21
|
+
Given(:normalizer) { Demalizer.new }
|
22
|
+
|
23
|
+
context "normalization" do
|
24
|
+
When(:normalized) { normalizer.normalize(data) }
|
25
|
+
|
26
|
+
context "maps specified keys" do
|
27
|
+
Then { normalized[:world].should == "COORDINATES" }
|
28
|
+
And { normalized[:roads].should == 50 }
|
29
|
+
end
|
30
|
+
|
31
|
+
context "does not pass through unspecified keys" do
|
32
|
+
Then { normalized[:cool].should be_nil }
|
33
|
+
And { normalized[:beasts].should be_nil }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "additional fields" do
|
38
|
+
Given { normalizer.class.include_fields [:beasts, :cool] }
|
39
|
+
When(:normalized) { normalizer.normalize(data) }
|
40
|
+
|
41
|
+
context "can be specified" do
|
42
|
+
Then { normalized[:cool].should == "true" }
|
43
|
+
And { normalized[:beasts].should == "1337" }
|
44
|
+
end
|
45
|
+
|
46
|
+
context "converts all keys to symbols" do
|
47
|
+
Then { normalized[:cool].should_not be_nil }
|
48
|
+
And { normalized[:beasts].should_not be_nil }
|
49
|
+
And { normalized["Middle Earth"].should be_nil }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "can mine nested properties" do
|
54
|
+
class BucketNormalizer < Schemad::Normalizer
|
55
|
+
normalize :answer_to_the_universe, key: "useless_root"
|
56
|
+
normalize :username, key: "author/user/username"
|
57
|
+
normalize :avatar_url, key: "author/user/links/avatar/href"
|
58
|
+
normalize :email, key: "author/raw" do |value|
|
59
|
+
value.match(/\A[\w|\s]+<(.+)>\z/).captures.first
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class BadNormalizer < Schemad::Normalizer
|
64
|
+
# missing the "links" part of the path
|
65
|
+
normalize :avatar_url, key: "author/user/avatar/href"
|
66
|
+
end
|
67
|
+
|
68
|
+
Given(:data) {
|
69
|
+
{ useless_root: 42,
|
70
|
+
author: {
|
71
|
+
raw: "Joseph Walton <jwalton@atlassian.com>",
|
72
|
+
user: {
|
73
|
+
username: "jwalton",
|
74
|
+
display_name: "Joseph Walton",
|
75
|
+
links: {
|
76
|
+
self: {
|
77
|
+
href: "https://api.bitbucket.org/2.0/users/jwalton"
|
78
|
+
},
|
79
|
+
avatar: {
|
80
|
+
href: "funk_blue.png"
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
} }
|
86
|
+
|
87
|
+
context "parses paths" do
|
88
|
+
Given(:normalizer) { BucketNormalizer.new }
|
89
|
+
When(:results) { normalizer.normalize(data) }
|
90
|
+
Then { results[:username].should == "jwalton" }
|
91
|
+
And { results[:avatar_url].should == "funk_blue.png" }
|
92
|
+
And { results[:email].should == "jwalton@atlassian.com" }
|
93
|
+
And { results[:answer_to_the_universe].should == 42 }
|
94
|
+
end
|
95
|
+
|
96
|
+
context "bad path throws exception" do
|
97
|
+
Given(:normalizer) { BadNormalizer.new }
|
98
|
+
When(:results) { normalizer.normalize(data) }
|
99
|
+
Then { expect(results).to have_failed(Schemad::Normalizer::InvalidPath, /author\/user\/avatar\/href/) }
|
100
|
+
end
|
101
|
+
|
102
|
+
context "can allow fields from nested data" do
|
103
|
+
Given { BucketNormalizer.include_fields ["author/user/display_name"] }
|
104
|
+
Given(:normalizer) { BucketNormalizer.new }
|
105
|
+
When(:results) { normalizer.normalize(data) }
|
106
|
+
Then { results[:display_name].should == "Joseph Walton" }
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'schemad/type_handler'
|
4
|
+
require 'schemad/abstract_handler'
|
5
|
+
|
6
|
+
require 'schemad/types/boolean_handler'
|
7
|
+
require 'schemad/types/string_handler'
|
8
|
+
require 'schemad/types/time_handler'
|
9
|
+
require 'schemad/types/integer_handler'
|
10
|
+
|
11
|
+
describe Schemad::TypeHandler do
|
12
|
+
|
13
|
+
context "rejects unknown types" do
|
14
|
+
When(:typer) { Schemad::TypeHandler.new(:fake_type) }
|
15
|
+
Then { expect(typer).to have_failed(Schemad::TypeHandler::UnknownHandler, /No known handlers for FakeType/) }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "defaults to string type" do
|
19
|
+
When(:typer) { Schemad::TypeHandler.new }
|
20
|
+
Then { expect(typer.instance_variable_get(:@handler)).to be_a(Schemad::StringHandler) }
|
21
|
+
end
|
22
|
+
|
23
|
+
context "can register custom handlers" do
|
24
|
+
class YouMomHandler < Schemad::AbstractHandler
|
25
|
+
handle :your_mom
|
26
|
+
|
27
|
+
def parse(value)
|
28
|
+
"Your Mom"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Given { Schemad::TypeHandler.register(YouMomHandler) }
|
33
|
+
Given(:typer) { Schemad::TypeHandler.new(:your_mom) }
|
34
|
+
When(:parsed) { typer.parse("Good vs. Evil") }
|
35
|
+
Then { parsed.should == "Your Mom" }
|
36
|
+
end
|
37
|
+
|
38
|
+
context "can handle bools" do
|
39
|
+
Given(:bool_handler) { Schemad::TypeHandler.new(:boolean) }
|
40
|
+
|
41
|
+
context "knows trues" do
|
42
|
+
Schemad::BooleanHandler::VALID_TRUTHS.each do |val|
|
43
|
+
When(:parsed) { bool_handler.parse(val) }
|
44
|
+
Then { parsed.should be_true }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "rejects falses" do
|
49
|
+
[42, "Hello World", nil, String].each do |val|
|
50
|
+
When(:parsed) { bool_handler.parse(val) }
|
51
|
+
Then { parsed.should be_false }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "can parse integers" do
|
57
|
+
Given(:int_handler) { Schemad::TypeHandler.new(:integer) }
|
58
|
+
|
59
|
+
context "true to 1" do
|
60
|
+
When(:result) { int_handler.parse(true) }
|
61
|
+
Then { result.should eq(1) }
|
62
|
+
end
|
63
|
+
|
64
|
+
context "false to 0" do
|
65
|
+
When(:result) { int_handler.parse(false) }
|
66
|
+
Then { result.should eq(0) }
|
67
|
+
end
|
68
|
+
|
69
|
+
context "strings to numbers" do
|
70
|
+
When(:result) { int_handler.parse("42") }
|
71
|
+
Then { result.should == 42 }
|
72
|
+
end
|
73
|
+
|
74
|
+
context "odd strings to 0" do
|
75
|
+
When(:result) { int_handler.parse("sneeze") }
|
76
|
+
Then { result.should == 0 }
|
77
|
+
end
|
78
|
+
|
79
|
+
context "nil for unknown items" do
|
80
|
+
When(:result) { int_handler.parse(String) }
|
81
|
+
Then { result.should == nil }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "can parse dates" do
|
86
|
+
Given(:date_handler) { Schemad::TypeHandler.new(:date_time) }
|
87
|
+
Given(:time) { DateTime.now.to_time }
|
88
|
+
|
89
|
+
context "knows unix time" do
|
90
|
+
When(:result) { date_handler.parse(time.to_i) }
|
91
|
+
Then { result.to_i.should == time.to_i }
|
92
|
+
end
|
93
|
+
|
94
|
+
context "knows iso8601 time" do
|
95
|
+
When(:result) { date_handler.parse(time.iso8601) }
|
96
|
+
Then { result.to_i.should == time.to_i }
|
97
|
+
end
|
98
|
+
|
99
|
+
context "knows ruby date format time" do
|
100
|
+
When(:result) { date_handler.parse(time.to_s) }
|
101
|
+
Then { result.to_i.should == time.to_i }
|
102
|
+
end
|
103
|
+
|
104
|
+
context "knows ruby date object" do
|
105
|
+
Given(:date) { Date.today }
|
106
|
+
When(:result) { date_handler.parse(date) }
|
107
|
+
Then { result.should be_a(Time) }
|
108
|
+
And { result.to_i.should == date.to_time.to_i }
|
109
|
+
end
|
110
|
+
|
111
|
+
context "knows ruby time objects" do
|
112
|
+
Given(:time) { Time.now }
|
113
|
+
When(:result) { date_handler.parse(time) }
|
114
|
+
Then { result.should be_a(Time) }
|
115
|
+
Then { result.to_i.should == time.to_i }
|
116
|
+
end
|
117
|
+
|
118
|
+
context "knows ruby time objects" do
|
119
|
+
Given(:date_time) { DateTime.now }
|
120
|
+
When(:result) { date_handler.parse(date_time) }
|
121
|
+
Then { result.should be_a(Time) }
|
122
|
+
Then { result.to_i.should == date_time.to_time.to_i }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: schemad
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luke van der Hoeven
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: "\n This gem allows easy attribute definition, type casting and special
|
56
|
+
handling\n of data returned from a service of some kind. It's meant to be incredibly
|
57
|
+
general\n purpose. Maybe this is a bad idea...\n "
|
58
|
+
email:
|
59
|
+
- hungerandthirst@gmail.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- lib/schemad.rb
|
70
|
+
- lib/schemad/abstract_handler.rb
|
71
|
+
- lib/schemad/entity.rb
|
72
|
+
- lib/schemad/extensions.rb
|
73
|
+
- lib/schemad/normalizer.rb
|
74
|
+
- lib/schemad/type_handler.rb
|
75
|
+
- lib/schemad/types/boolean_handler.rb
|
76
|
+
- lib/schemad/types/integer_handler.rb
|
77
|
+
- lib/schemad/types/string_handler.rb
|
78
|
+
- lib/schemad/types/time_handler.rb
|
79
|
+
- lib/schemad/version.rb
|
80
|
+
- schemad.gemspec
|
81
|
+
- spec/entity_spec.rb
|
82
|
+
- spec/extensions_spec.rb
|
83
|
+
- spec/fixtures/demo_class.rb
|
84
|
+
- spec/normalizer_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
- spec/type_handler_spec.rb
|
87
|
+
homepage: ''
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 2.2.2
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: Simple schema DSL for services
|
111
|
+
test_files:
|
112
|
+
- spec/entity_spec.rb
|
113
|
+
- spec/extensions_spec.rb
|
114
|
+
- spec/fixtures/demo_class.rb
|
115
|
+
- spec/normalizer_spec.rb
|
116
|
+
- spec/spec_helper.rb
|
117
|
+
- spec/type_handler_spec.rb
|