arstotzka 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +13 -0
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/Guardfile +13 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/Rakefile +7 -0
- data/arstotzka.gemspec +29 -0
- data/docker-compose.yml +18 -0
- data/lib/arstotzka.rb +16 -0
- data/lib/arstotzka/builder.rb +73 -0
- data/lib/arstotzka/class_methods.rb +7 -0
- data/lib/arstotzka/crawler.rb +45 -0
- data/lib/arstotzka/exception.rb +3 -0
- data/lib/arstotzka/fetcher.rb +50 -0
- data/lib/arstotzka/reader.rb +44 -0
- data/lib/arstotzka/type_cast.rb +15 -0
- data/lib/arstotzka/version.rb +3 -0
- data/lib/arstotzka/wrapper.rb +36 -0
- data/spec/fixtures/accounts.json +27 -0
- data/spec/fixtures/accounts_missing.json +23 -0
- data/spec/fixtures/arstotzka.json +38 -0
- data/spec/fixtures/complete_person.json +9 -0
- data/spec/fixtures/person.json +5 -0
- data/spec/integration/readme/default_spec.rb +37 -0
- data/spec/integration/readme/my_parser_spec.rb +86 -0
- data/spec/lib/arstotzka/builder_spec.rb +100 -0
- data/spec/lib/arstotzka/crawler_spec.rb +276 -0
- data/spec/lib/arstotzka/fetcher_spec.rb +96 -0
- data/spec/lib/arstotzka/reader_spec.rb +120 -0
- data/spec/lib/arstotzka/wrapper_spec.rb +121 -0
- data/spec/lib/arstotzka_spec.rb +129 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/fixture_helpers.rb +19 -0
- data/spec/support/models.rb +5 -0
- data/spec/support/models/arstotzka/dummy.rb +23 -0
- data/spec/support/models/arstotzka/fetcher/dummy.rb +3 -0
- data/spec/support/models/arstotzka/type_cast.rb +6 -0
- data/spec/support/models/arstotzka/wrapper/dummy.rb +7 -0
- data/spec/support/models/game.rb +11 -0
- data/spec/support/models/house.rb +11 -0
- data/spec/support/models/my_parser.rb +28 -0
- data/spec/support/models/person.rb +8 -0
- data/spec/support/models/star.rb +7 -0
- data/spec/support/models/star_gazer.rb +13 -0
- data/spec/support/shared_examples/wrapper.rb +19 -0
- metadata +229 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 15a7631fb286cccc364034583007f5a8297fd060
|
4
|
+
data.tar.gz: 21421c75c92d86840b2a622de626e4d83ea58998
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a37e563c29470385399555ac5000977a31d6ced5ebc65eed86289426ff1a55e2e2aa96544525c84a1583ec7fb242c942520cebf590354468ad1966980b6c287
|
7
|
+
data.tar.gz: b03337d691634b834270b080ced42eb495189f9ddb5d63bd239b6714cf4ede664ae41d8a9d5de76de0c7e751fa1b78baa4e74194a2d85d4fb8179d45631a8f14
|
@@ -0,0 +1,13 @@
|
|
1
|
+
version: 2
|
2
|
+
jobs:
|
3
|
+
build:
|
4
|
+
docker:
|
5
|
+
- image: circleci/ruby:2.4.1
|
6
|
+
steps:
|
7
|
+
- checkout
|
8
|
+
- run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
9
|
+
- run: chmod +x ./cc-test-reporter
|
10
|
+
- run: ./cc-test-reporter before-build
|
11
|
+
- run: bundle install
|
12
|
+
- run: bundle exec rspec
|
13
|
+
- run: ./cc-test-reporter after-build --exit-code $?
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# More info at https://github.com/guard/guard#readme
|
2
|
+
|
3
|
+
guard 'bundler' do
|
4
|
+
watch('Gemfile')
|
5
|
+
watch(/^.+\.gemspec/)
|
6
|
+
end
|
7
|
+
|
8
|
+
guard :rspec, all_after_pass: true, all_on_start: true do
|
9
|
+
watch(%r{^spec/.+_spec\.rb$})
|
10
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
11
|
+
watch('spec/spec_helper.rb') { "spec" }
|
12
|
+
end
|
13
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Favini
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
Arstotzka
|
2
|
+
========
|
3
|
+
[](https://codeclimate.com/github/darthjee/arstotzka)
|
4
|
+
[](https://codeclimate.com/github/darthjee/arstotzka/coverage)
|
5
|
+
[](https://codeclimate.com/github/darthjee/arstotzka)
|
6
|
+
|
7
|
+
This project allows for a quick hash / json data fetching in order to avoid code
|
8
|
+
that tries to crawl through a hash and has to constantly check for nil values or missing keys
|
9
|
+
|
10
|
+
also, this concern, like openstruct, allow the json to be manipulated as an object, but
|
11
|
+
avoids method missing by aways having the declarated methods, even if that means nil return
|
12
|
+
|
13
|
+
Json Parser is also usefull when you need keys case changed or data type cast
|
14
|
+
|
15
|
+
Getting started
|
16
|
+
---------------
|
17
|
+
1. Add Arstotzka to your `Gemfile` and `bundle install`:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'arstotzka'
|
21
|
+
```
|
22
|
+
|
23
|
+
2. Include in a class that you want to wrap a json/hash
|
24
|
+
```ruby
|
25
|
+
class MyParser
|
26
|
+
include Arstotzka
|
27
|
+
```
|
28
|
+
|
29
|
+
3. Declare the keys you want to crawl
|
30
|
+
```ruby
|
31
|
+
class MyParser
|
32
|
+
include Arstotzka
|
33
|
+
|
34
|
+
expose :id
|
35
|
+
expose :name, :age, path: :person
|
36
|
+
|
37
|
+
attr_reader :json
|
38
|
+
|
39
|
+
def initialize(json = {})
|
40
|
+
@json = json
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
```
|
45
|
+
|
46
|
+
and let it fetch values from your hash
|
47
|
+
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
object = MyParser.new(
|
51
|
+
id: 10,
|
52
|
+
age: 22
|
53
|
+
person: {
|
54
|
+
name: 'Robert',
|
55
|
+
age: 22
|
56
|
+
}
|
57
|
+
)
|
58
|
+
|
59
|
+
object.name
|
60
|
+
#returns 'Robert'
|
61
|
+
```
|
62
|
+
|
63
|
+
this is usefull when trying to fetch data from hashes missing nodes
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
MyParser.new.name
|
67
|
+
#returns nil
|
68
|
+
```
|
69
|
+
|
70
|
+
4. fully customise the way you crawl / fetch the information with [Options](#options)
|
71
|
+
|
72
|
+
5. Create custom [typecast](#TypeCast)
|
73
|
+
|
74
|
+
Options
|
75
|
+
-------
|
76
|
+
- path: path where to find the sub hash that contains the key (empty by default)
|
77
|
+
- json: method that contains the hash to be parsed (json by default)
|
78
|
+
- full_path: full path to fetch the value (empty by default)
|
79
|
+
- cached: indicator that once the value has been fetched, it should be cached (false by default)
|
80
|
+
- class: class to be used when wrapping the final value
|
81
|
+
- compact: indicator telling to ignore nil values inside array (false by default)
|
82
|
+
- flatten: indicator telling that to flattern the resulting array (false by default)
|
83
|
+
- after: name of a method to be called after with the resulting value
|
84
|
+
- case: case of the keys from the json (camel by default)
|
85
|
+
- type: Type that the value must be cast into ([TypeCast](#typecast))
|
86
|
+
- default: Default value (prior to casting and wrapping, see [Default](#default))
|
87
|
+
|
88
|
+
## TypeCast
|
89
|
+
The type casting, when the option `type` is passed, is done through the `Arstotzka::TypeCast` which can
|
90
|
+
be extended
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
module Arstotzka::TypeCast
|
94
|
+
def to_money_float(value)
|
95
|
+
value.gsub(/\$ */, '').to_f
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class MyParser
|
102
|
+
include Arstotzka
|
103
|
+
|
104
|
+
expose :total_money, full_path: 'accounts.balance', after: :sum,
|
105
|
+
cached: true, type: :money_float
|
106
|
+
expose :total_owed, full_path: 'loans.value', after: :sum,
|
107
|
+
cached: true, type: :money_float
|
108
|
+
|
109
|
+
attr_reader :json
|
110
|
+
|
111
|
+
def initialize(json = {})
|
112
|
+
@json = json
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
#this method will receive the array of values resulting from the initial mapping
|
118
|
+
def sum(balances)
|
119
|
+
balances.sum if balances
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
object = MyParser.new(
|
126
|
+
accounts: [
|
127
|
+
{ balance: '$ 1000.50', type: 'checking' },
|
128
|
+
{ balance: '$ 150.10', type: 'savings' },
|
129
|
+
{ balance: '$ -100.24', type: 'checking' }
|
130
|
+
],
|
131
|
+
loans: [
|
132
|
+
{ value: '$ 300.50', bank: 'the_bank' },
|
133
|
+
{ value: '$ 150.10', type: 'the_other_bank' },
|
134
|
+
{ value: '$ 100.24', type: 'the_same_bank' }
|
135
|
+
]
|
136
|
+
)
|
137
|
+
|
138
|
+
object.balance
|
139
|
+
#returns 1050.36
|
140
|
+
```
|
141
|
+
|
142
|
+
## Default
|
143
|
+
Default value returned before typecasting or class wrapping
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class Star
|
147
|
+
attr_reader :name
|
148
|
+
|
149
|
+
def initialize(name:)
|
150
|
+
@name = name
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class StarGazer
|
155
|
+
include Arstotzka
|
156
|
+
|
157
|
+
expose :favorite_star, full_path: 'universe.star',
|
158
|
+
default: { name: 'Sun' }, class: ::Star
|
159
|
+
|
160
|
+
attr_reader :json
|
161
|
+
|
162
|
+
def initialize(json = {})
|
163
|
+
@json = json
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
```
|
168
|
+
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
star_gazer = StarGazer.new
|
172
|
+
|
173
|
+
star_gazer.favorite_star.name
|
174
|
+
#returns "Sun"
|
175
|
+
|
176
|
+
star_gazer.favorite_star.class
|
177
|
+
#returns Star
|
178
|
+
```
|
179
|
+
|
data/Rakefile
ADDED
data/arstotzka.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 'arstotzka/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "arstotzka"
|
8
|
+
spec.version = Arstotzka::VERSION
|
9
|
+
spec.authors = ["Bidu Dev's Team"]
|
10
|
+
spec.email = ["dev@bidu.com.br"]
|
11
|
+
spec.summary = "Json Parser"
|
12
|
+
spec.description = spec.description
|
13
|
+
spec.homepage = "https://github.com/Bidu/arstotzka"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency 'activesupport', '~> 5.x'
|
21
|
+
spec.add_runtime_dependency 'sinclair'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'safe_attribute_assignment'
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
25
|
+
spec.add_development_dependency "rake", ">= 12.3.1"
|
26
|
+
spec.add_development_dependency "rspec", ">= 3.7"
|
27
|
+
spec.add_development_dependency 'simplecov'
|
28
|
+
spec.add_development_dependency 'pry-nav'
|
29
|
+
end
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
version: '2'
|
2
|
+
services:
|
3
|
+
base: &base
|
4
|
+
image: ruby:2.4.0
|
5
|
+
working_dir: /home/app/arstotzka
|
6
|
+
volumes:
|
7
|
+
- .:/home/app/arstotzka
|
8
|
+
- arstotzka_gems_2_4_0:/usr/local/bundle
|
9
|
+
|
10
|
+
#################### CONTAINERS ####################
|
11
|
+
|
12
|
+
arstotzka:
|
13
|
+
<<: *base
|
14
|
+
container_name: arstotzka
|
15
|
+
command: /bin/bash -c 'bundle install && bundle exec rspec'
|
16
|
+
|
17
|
+
volumes:
|
18
|
+
arstotzka_gems_2_4_0:
|
data/lib/arstotzka.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'sinclair'
|
4
|
+
|
5
|
+
module Arstotzka
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
autoload :Builder, 'arstotzka/builder'
|
9
|
+
autoload :ClassMethods, 'arstotzka/class_methods'
|
10
|
+
autoload :Crawler, 'arstotzka/crawler'
|
11
|
+
autoload :Exception, 'arstotzka/exception'
|
12
|
+
autoload :Fetcher, 'arstotzka/fetcher'
|
13
|
+
autoload :Reader, 'arstotzka/reader'
|
14
|
+
autoload :Wrapper, 'arstotzka/wrapper'
|
15
|
+
autoload :TypeCast, 'arstotzka/type_cast'
|
16
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class Arstotzka::Builder < Sinclair
|
2
|
+
|
3
|
+
attr_reader :attr_names, :path, :full_path, :cached
|
4
|
+
|
5
|
+
def initialize(attr_names, clazz, path: nil, full_path: nil, cached: false, **options)
|
6
|
+
super(clazz, {
|
7
|
+
after: false,
|
8
|
+
case: :lower_camel,
|
9
|
+
class: nil,
|
10
|
+
compact: false,
|
11
|
+
default: nil,
|
12
|
+
flatten: false,
|
13
|
+
json: :json,
|
14
|
+
type: :none
|
15
|
+
}.merge(options.symbolize_keys))
|
16
|
+
|
17
|
+
@attr_names = attr_names
|
18
|
+
@path = path
|
19
|
+
@full_path = full_path
|
20
|
+
@cached = cached
|
21
|
+
init
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def init
|
27
|
+
attr_names.each do |attr|
|
28
|
+
add_attr(attr)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def real_path(attribute)
|
33
|
+
full_path || [path, attribute].compact.join('.')
|
34
|
+
end
|
35
|
+
|
36
|
+
def json_name
|
37
|
+
options[:json]
|
38
|
+
end
|
39
|
+
|
40
|
+
def wrapper_clazz
|
41
|
+
options[:class]
|
42
|
+
end
|
43
|
+
|
44
|
+
def case_type
|
45
|
+
options[:case]
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetcher_options(attribute)
|
49
|
+
options.slice(:compact, :after, :type, :flatten, :default).merge({
|
50
|
+
clazz: wrapper_clazz,
|
51
|
+
case_type: case_type,
|
52
|
+
path: real_path(attribute)
|
53
|
+
})
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_attr(attribute)
|
57
|
+
add_method attribute, "#{cached ? cached_fetcher(attribute) : attr_fetcher(attribute)}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def attr_fetcher(attribute)
|
61
|
+
<<-CODE
|
62
|
+
::Arstotzka::Fetcher.new(
|
63
|
+
#{json_name}, self, #{fetcher_options(attribute)}
|
64
|
+
).fetch
|
65
|
+
CODE
|
66
|
+
end
|
67
|
+
|
68
|
+
def cached_fetcher(attribute)
|
69
|
+
<<-CODE
|
70
|
+
@#{attribute} ||= #{attr_fetcher(attribute)}
|
71
|
+
CODE
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Arstotzka
|
2
|
+
class Crawler
|
3
|
+
attr_reader :post_process, :path, :case_type, :compact, :default
|
4
|
+
|
5
|
+
def initialize(path:, case_type: :lower_camel, compact: false, default: nil, &block)
|
6
|
+
@case_type = case_type
|
7
|
+
@compact = compact
|
8
|
+
@default = default
|
9
|
+
@path = path
|
10
|
+
@post_process = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def value(json, index = 0)
|
14
|
+
crawl(json, index)
|
15
|
+
rescue Exception::KeyNotFound
|
16
|
+
wrap(default)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def crawl(json, index = 0)
|
22
|
+
return wrap(json) if reader.is_ended?(index)
|
23
|
+
return crawl_array(json, index) if json.is_a?(Array)
|
24
|
+
|
25
|
+
crawl(reader.read(json, index), index + 1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def reader
|
29
|
+
@reader ||= Arstotzka::Reader.new(
|
30
|
+
path: path,
|
31
|
+
case_type: case_type
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def wrap(json)
|
36
|
+
post_process.call(json)
|
37
|
+
end
|
38
|
+
|
39
|
+
def crawl_array(array, index)
|
40
|
+
array.map { |j| value(j, index) }.tap do |a|
|
41
|
+
a.compact! if compact
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|