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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +13 -0
  3. data/.gitignore +3 -0
  4. data/.rspec +1 -0
  5. data/Gemfile +8 -0
  6. data/Guardfile +13 -0
  7. data/LICENSE +21 -0
  8. data/README.md +179 -0
  9. data/Rakefile +7 -0
  10. data/arstotzka.gemspec +29 -0
  11. data/docker-compose.yml +18 -0
  12. data/lib/arstotzka.rb +16 -0
  13. data/lib/arstotzka/builder.rb +73 -0
  14. data/lib/arstotzka/class_methods.rb +7 -0
  15. data/lib/arstotzka/crawler.rb +45 -0
  16. data/lib/arstotzka/exception.rb +3 -0
  17. data/lib/arstotzka/fetcher.rb +50 -0
  18. data/lib/arstotzka/reader.rb +44 -0
  19. data/lib/arstotzka/type_cast.rb +15 -0
  20. data/lib/arstotzka/version.rb +3 -0
  21. data/lib/arstotzka/wrapper.rb +36 -0
  22. data/spec/fixtures/accounts.json +27 -0
  23. data/spec/fixtures/accounts_missing.json +23 -0
  24. data/spec/fixtures/arstotzka.json +38 -0
  25. data/spec/fixtures/complete_person.json +9 -0
  26. data/spec/fixtures/person.json +5 -0
  27. data/spec/integration/readme/default_spec.rb +37 -0
  28. data/spec/integration/readme/my_parser_spec.rb +86 -0
  29. data/spec/lib/arstotzka/builder_spec.rb +100 -0
  30. data/spec/lib/arstotzka/crawler_spec.rb +276 -0
  31. data/spec/lib/arstotzka/fetcher_spec.rb +96 -0
  32. data/spec/lib/arstotzka/reader_spec.rb +120 -0
  33. data/spec/lib/arstotzka/wrapper_spec.rb +121 -0
  34. data/spec/lib/arstotzka_spec.rb +129 -0
  35. data/spec/spec_helper.rb +23 -0
  36. data/spec/support/fixture_helpers.rb +19 -0
  37. data/spec/support/models.rb +5 -0
  38. data/spec/support/models/arstotzka/dummy.rb +23 -0
  39. data/spec/support/models/arstotzka/fetcher/dummy.rb +3 -0
  40. data/spec/support/models/arstotzka/type_cast.rb +6 -0
  41. data/spec/support/models/arstotzka/wrapper/dummy.rb +7 -0
  42. data/spec/support/models/game.rb +11 -0
  43. data/spec/support/models/house.rb +11 -0
  44. data/spec/support/models/my_parser.rb +28 -0
  45. data/spec/support/models/person.rb +8 -0
  46. data/spec/support/models/star.rb +7 -0
  47. data/spec/support/models/star_gazer.rb +13 -0
  48. data/spec/support/shared_examples/wrapper.rb +19 -0
  49. metadata +229 -0
@@ -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 $?
@@ -0,0 +1,3 @@
1
+ coverage
2
+ Gemfile.lock
3
+
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'safe_attribute_assignment', '0.0.3'
7
+ end
8
+
@@ -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.
@@ -0,0 +1,179 @@
1
+ Arstotzka
2
+ ========
3
+ [![Code Climate](https://codeclimate.com/github/darthjee/arstotzka/badges/gpa.svg)](https://codeclimate.com/github/darthjee/arstotzka)
4
+ [![Test Coverage](https://codeclimate.com/github/darthjee/arstotzka/badges/coverage.svg)](https://codeclimate.com/github/darthjee/arstotzka/coverage)
5
+ [![Issue Count](https://codeclimate.com/github/darthjee/arstotzka/badges/issue_count.svg)](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
+
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task test: :spec
@@ -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
@@ -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:
@@ -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,7 @@
1
+ module Arstotzka
2
+ module ClassMethods
3
+ def expose(*attr_names, **options)
4
+ Builder.new(attr_names, self, options).build
5
+ end
6
+ end
7
+ 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