ossert 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.rubocop_todo.yml +44 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +16 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +199 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/config/classifiers.yml +153 -0
  15. data/config/descriptions.yml +45 -0
  16. data/config/sidekiq.rb +15 -0
  17. data/config/stats.yml +198 -0
  18. data/config/translations.yml +44 -0
  19. data/db/backups/.keep +0 -0
  20. data/db/migrate/001_create_projects.rb +22 -0
  21. data/db/migrate/002_create_exceptions.rb +14 -0
  22. data/db/migrate/003_add_meta_to_projects.rb +14 -0
  23. data/db/migrate/004_add_timestamps_to_projects.rb +12 -0
  24. data/db/migrate/005_create_classifiers.rb +19 -0
  25. data/lib/ossert/classifiers/decision_tree.rb +112 -0
  26. data/lib/ossert/classifiers/growing/check.rb +172 -0
  27. data/lib/ossert/classifiers/growing/classifier.rb +175 -0
  28. data/lib/ossert/classifiers/growing.rb +163 -0
  29. data/lib/ossert/classifiers.rb +14 -0
  30. data/lib/ossert/config.rb +24 -0
  31. data/lib/ossert/fetch/bestgems.rb +98 -0
  32. data/lib/ossert/fetch/github.rb +536 -0
  33. data/lib/ossert/fetch/rubygems.rb +80 -0
  34. data/lib/ossert/fetch.rb +142 -0
  35. data/lib/ossert/presenters/project.rb +202 -0
  36. data/lib/ossert/presenters/project_v2.rb +117 -0
  37. data/lib/ossert/presenters.rb +8 -0
  38. data/lib/ossert/project.rb +144 -0
  39. data/lib/ossert/quarters_store.rb +164 -0
  40. data/lib/ossert/rake_tasks.rb +6 -0
  41. data/lib/ossert/reference.rb +87 -0
  42. data/lib/ossert/repositories.rb +138 -0
  43. data/lib/ossert/saveable.rb +153 -0
  44. data/lib/ossert/stats/agility_quarter.rb +62 -0
  45. data/lib/ossert/stats/agility_total.rb +71 -0
  46. data/lib/ossert/stats/base.rb +113 -0
  47. data/lib/ossert/stats/community_quarter.rb +28 -0
  48. data/lib/ossert/stats/community_total.rb +24 -0
  49. data/lib/ossert/stats.rb +32 -0
  50. data/lib/ossert/tasks/database.rake +179 -0
  51. data/lib/ossert/tasks/ossert.rake +52 -0
  52. data/lib/ossert/version.rb +4 -0
  53. data/lib/ossert/workers/fetch.rb +21 -0
  54. data/lib/ossert/workers/fetch_bestgems_page.rb +32 -0
  55. data/lib/ossert/workers/refresh_fetch.rb +22 -0
  56. data/lib/ossert/workers/sync_rubygems.rb +0 -0
  57. data/lib/ossert/workers.rb +11 -0
  58. data/lib/ossert.rb +63 -0
  59. data/ossert.gemspec +47 -0
  60. metadata +396 -0
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+ require 'benchmark'
5
+
6
+ require 'ossert/fetch/github'
7
+ require 'ossert/fetch/rubygems'
8
+ require 'ossert/fetch/bestgems'
9
+
10
+ # TODO: Add logging
11
+
12
+ module Ossert
13
+ # Public: Various classes and methods for fetching data from different sources.
14
+ # Such as GitHub, Rubygems, Bestgems. Also provides simple functionality for
15
+ # fetching HTTP API.
16
+ module Fetch
17
+ ALL_FETCHERS = [Rubygems, Bestgems, GitHub].freeze
18
+
19
+ # Public: Fetch data for project using all fetchers by default process method
20
+ #
21
+ # project - The Ossert::Project instance to fill using fetchers
22
+ #
23
+ # Examples
24
+ #
25
+ # project = Ossert::Project.new('ramaze')
26
+ # Ossert::Fetch.all(project)
27
+ # project.dump
28
+ #
29
+ # Returns nothing.
30
+ def all(project)
31
+ ALL_FETCHERS.each do |fetcher|
32
+ puts "======> with #{fetcher}..."
33
+ time = Benchmark.realtime do
34
+ fetcher.new(project).process
35
+ end
36
+ puts "<====== Finished in #{time.round(3)} sec."
37
+ sleep(1)
38
+ end
39
+ nil
40
+ rescue => e
41
+ puts "Fetching Failed for '#{name}' with error: #{e.inspect}"
42
+ puts e.backtrace
43
+ end
44
+ module_function :all
45
+
46
+ # Public: Fetch data for project using given fetchers by process method
47
+ #
48
+ # fetchers - The Array or one of Ossert::Fetch::GitHub, Ossert::Fetch::Bestgems, Ossert::Fetch::Rubygems to
49
+ # use for processing
50
+ # project - The Ossert::Project instance to fill using fetchers
51
+ # process - The Symbol method name used for processing by fetchers (default: :process)
52
+ #
53
+ # Examples
54
+ #
55
+ # project = Ossert::Project.new('ramaze')
56
+ # Ossert::Fetch.only(Ossert::Fetch::Rubygems, project, :process_meta)
57
+ # project.dump_attribute :meta_data
58
+ #
59
+ # Returns nothing.
60
+ def only(fetchers, project, process = :process)
61
+ fetchers = Array.wrap(fetchers)
62
+ puts "Fetching project '#{project.name}'..."
63
+ (ALL_FETCHERS & fetchers).each do |fetcher|
64
+ puts "======> with #{fetcher}..."
65
+ time = Benchmark.realtime do
66
+ fetcher.new(project).send(process)
67
+ end
68
+ puts "<====== Finished in #{time.round(3)} sec."
69
+ sleep(1)
70
+ end
71
+ end
72
+ module_function :only
73
+
74
+ # Public: Simple client for fetching HTTP API
75
+ #
76
+ # Examples
77
+ #
78
+ # client = SimpleClient.new("http://bestgems.org/api/v1/")
79
+ # client.get("gems/#{project.rubygems_alias}/total_downloads.json")
80
+ # # => Some JSON from api
81
+ class SimpleClient
82
+ attr_reader :api_endpoint, :type
83
+
84
+ # Public: Instantiate client for fetching API for given api_endpoint and response type
85
+ #
86
+ # path - The String describes path of endpoint to access the data
87
+ # type - The String describes type of response data, e.g. 'json'
88
+ #
89
+ # Examples
90
+ #
91
+ # client = SimpleClient.new("http://bestgems.org/api/v1/")
92
+ # client.get("gems/#{project.rubygems_alias}/total_downloads.json")
93
+ # # => Some JSON from api
94
+ #
95
+ # Returns nothing.
96
+ def initialize(api_endpoint, type = nil)
97
+ raise ArgumentError if !api_endpoint.start_with?('http') || !api_endpoint.end_with?('/')
98
+ @api_endpoint = api_endpoint
99
+ @type = type || 'json'
100
+ end
101
+
102
+ class NotFound < StandardError; end
103
+ class UnexpectedResponseError < StandardError; end
104
+
105
+ # Public: Get data via HTTP GET for given API path
106
+ #
107
+ # path - The String describes path of endpoint to access the data
108
+ #
109
+ # Examples
110
+ #
111
+ # client = SimpleClient.new("http://bestgems.org/api/v1/")
112
+ # client.get("gems/#{project.rubygems_alias}/total_downloads.json")
113
+ # # => Some JSON from api
114
+ #
115
+ # Returns nothing.
116
+ def get(path)
117
+ raise ArgumentError unless path.end_with? type
118
+ response = agent.get("#{api_endpoint}#{path}")
119
+ case response.status
120
+ when 404
121
+ raise NotFound
122
+ when 200
123
+ JSON.parse(response.body)
124
+ else
125
+ raise UnexpectedResponseError
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ # Internal: Initialize Faraday agent for processing requests
132
+ #
133
+ # Returns Faraday::Connection instance.
134
+ def agent
135
+ @agent ||= ::Faraday.new do |http|
136
+ http.request :url_encoded
137
+ http.adapter :net_http
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+ require 'ossert/presenters/project_v2'
3
+
4
+ module Ossert
5
+ module Presenters
6
+ class Project
7
+ include Ossert::Presenters::ProjectV2
8
+
9
+ attr_reader :project, :decorator
10
+
11
+ def initialize(project)
12
+ @project = project
13
+ @reference = Ossert::Classifiers::Growing.current.reference_values_per_grade
14
+ @decorator = Decorator.new(@reference)
15
+ end
16
+
17
+ def self.with_presenter(project)
18
+ presenter = new(project)
19
+ presenter.prepare!
20
+ yield(presenter)
21
+ presenter.cleanup_references!
22
+ end
23
+
24
+ class Decorator
25
+ def initialize(reference)
26
+ @reference = reference
27
+ end
28
+
29
+ # value, Float !
30
+ def with_reference(text, value, metric, type)
31
+ return (text.to_i.positive? ? "+#{text}" : text).to_s if type == :delta
32
+ metric_by_ref = @reference[type][metric.to_s]
33
+ reference = Project::CLASSES.inject('NaN') do |acc, ref|
34
+ metric_by_ref[ref][:range].cover?(value) ? ref : acc
35
+ end
36
+
37
+ { text: "#{text}&nbsp;#{Project::KLASS_2_GRADE[reference]}",
38
+ mark: Project::KLASS_2_GRADE[reference].downcase }
39
+ rescue => e
40
+ puts "NO DATA FOR METRIC: '#{metric}'"
41
+ raise e
42
+ end
43
+
44
+ TOO_LONG_AGO = 20.years.ago
45
+ METRICS_DECORATIONS = {
46
+ /(percent|divergence)/ => ->(value) { "#{value.ceil}%" },
47
+ /(date|changed)/ => (lambda do |value|
48
+ date = Time.at(value)
49
+ return 'N/A' if date < TOO_LONG_AGO
50
+ date.strftime('%d/%m/%y')
51
+ end),
52
+ /processed_in/ => (lambda do |value|
53
+ return 'N/A' if value >= Ossert::Stats::PER_YEAR_TOO_LONG || value.zero?
54
+ case value
55
+ when 1
56
+ "~#{value.ceil} day"
57
+ when 2..30
58
+ "~#{value.ceil} days"
59
+ when 31..61
60
+ "~#{(value / 31).to_i} month"
61
+ else
62
+ "~#{(value / 31).to_i} months"
63
+ end
64
+ end),
65
+ /period/ => (lambda do |value|
66
+ if (years = value.to_i / 365).positive?
67
+ "#{years}+ years"
68
+ 'Less than a year'
69
+ else
70
+ end
71
+ end),
72
+ /downloads/ => ->(value) { value.ceil.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse }
73
+ }.freeze
74
+
75
+ def value(metric, value)
76
+ value = value.to_f
77
+ METRICS_DECORATIONS.each { |check, decorator| return decorator.call(value) if metric =~ check }
78
+ value.to_i
79
+ end
80
+
81
+ def metric(metric, value, type)
82
+ with_reference(value(metric, value), value.to_f, metric, type)
83
+ end
84
+
85
+ def quarter_with_diff(project, time, section)
86
+ section_type = "#{section}_quarter".to_sym
87
+ quarter_data = project.send(section).quarters[quarter_start(time)].metrics_to_hash
88
+ diff = quarter_values(project.send(section).quarters[quarter_start(time) - 1.day].metrics_to_hash)
89
+
90
+ quarter_data.each_with_object({}) do |(metric, value), result|
91
+ decorated_metric = metric(metric, value, section_type)
92
+ result[Ossert.t(metric)] = <<-TEXT
93
+ #{decorated_metric[:text]} <> #{metric(metric, value.to_i - diff[metric], :delta)}
94
+ TEXT
95
+ end
96
+ end
97
+
98
+ def metrics(metrics_data, section_type)
99
+ metrics_data.each_with_object({}) do |(metric, value), result|
100
+ result[metric] = metric(metric, value.to_i, section_type)
101
+ end
102
+ end
103
+
104
+ def quarter_values(quarter_data)
105
+ quarter_data.each_with_object({}) do |(metric, value), res|
106
+ res[metric] = value.to_i
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def quarter_start(time)
113
+ Time.at(time).to_date.to_time(:utc).beginning_of_quarter
114
+ end
115
+ end
116
+
117
+ def agility_quarter(time)
118
+ decorator.quarter_with_diff(@project, time, :agility)
119
+ end
120
+
121
+ def community_quarter(time)
122
+ decorator.quarter_with_diff(@project, time, :community)
123
+ end
124
+
125
+ def agility_quarter_values(time)
126
+ decorator.quarter_values @project.agility.quarters[time].metrics_to_hash
127
+ end
128
+
129
+ def community_quarter_values(time)
130
+ decorator.quarter_values @project.community.quarters[time].metrics_to_hash
131
+ end
132
+
133
+ def agility_total
134
+ @agility_total ||= decorator.metrics @project.agility.total.metrics_to_hash, :agility_total
135
+ end
136
+
137
+ def community_total
138
+ @community_total ||= decorator.metrics @project.community.total.metrics_to_hash, :community_total
139
+ end
140
+
141
+ def community_last_year
142
+ @community_last_year ||= decorator.metrics @project.community.quarters.last_year_as_hash, :community_year
143
+ end
144
+
145
+ def agility_last_year
146
+ @agility_last_year ||= decorator.metrics @project.agility.quarters.last_year_as_hash, :agility_year
147
+ end
148
+
149
+ def metric_preview(metric)
150
+ preview = {}
151
+ return(preview) if (section = Ossert::Stats.guess_section_by_metric(metric)) == :not_found
152
+
153
+ preview[:tooltip] = MultiJson.dump(tooltip_data(metric))
154
+ preview[:translation] = Ossert.t(metric)
155
+
156
+ preview.merge!(last_year_section(metric, section))
157
+ preview.merge!(total_section(metric, section))
158
+ preview
159
+ end
160
+
161
+ def last_year_section(metric, section)
162
+ section_last_year = public_send("#{section}_last_year")[metric]
163
+ {
164
+ last_year_mark: section_last_year.try(:[], :mark),
165
+ last_year_val: section_last_year.try(:[], :text) || 'N/A'
166
+ }
167
+ end
168
+
169
+ def total_section(metric, section)
170
+ section_total = public_send("#{section}_total")[metric]
171
+ {
172
+ total_mark: section_total.try(:[], :mark),
173
+ total_val: section_total.try(:[], :text) || 'N/A'
174
+ }
175
+ end
176
+
177
+ def prepare!
178
+ agility_total
179
+ agility_last_year
180
+ community_total
181
+ community_last_year
182
+
183
+ lookback = 5
184
+ check_results = (lookback - 1).downto(0).map do |last_year_offset|
185
+ Ossert::Classifiers::Growing.current.check(@project, last_year_offset)
186
+ end
187
+
188
+ @grade = check_results.last(2).first.map { |k, v| [k, v[:mark].downcase] }.to_h
189
+ @fast_preview_graph = fast_preview_graph_data(lookback, check_results)
190
+ end
191
+ attr_reader :grade, :fast_preview_graph
192
+
193
+ def cleanup_references!
194
+ @reference = nil
195
+ @project = nil
196
+ @fast_preview_graph_data = nil
197
+ @grade = nil
198
+ @decorator = nil
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+ module Ossert
3
+ module Presenters
4
+ module ProjectV2
5
+ CLASSES = %w(
6
+ ClassE
7
+ ClassD
8
+ ClassC
9
+ ClassB
10
+ ClassA
11
+ ).freeze
12
+ KLASS_2_GRADE = {
13
+ 'ClassA' => 'A',
14
+ 'ClassB' => 'B',
15
+ 'ClassC' => 'C',
16
+ 'ClassD' => 'D',
17
+ 'ClassE' => 'E'
18
+ }.freeze
19
+
20
+ def preview_reference_values_for(metric, section) # maybe automatically find section?
21
+ metric_by_grades = @reference[section][metric.to_s]
22
+ grades = CLASSES.reverse
23
+ sign = reversed_metrics.include?(metric) ? '<' : '>'
24
+
25
+ grades.each_with_object({}) do |klass, preview|
26
+ preview[KLASS_2_GRADE[klass]] = "#{sign} #{metric_by_grades[klass][:threshold].to_i}"
27
+ end
28
+ end
29
+
30
+ REFERENCES_STUB = {
31
+ 'ClassA' => { threshold: '0', range: [] },
32
+ 'ClassB' => { threshold: '0', range: [] },
33
+ 'ClassC' => { threshold: '0', range: [] },
34
+ 'ClassD' => { threshold: '0', range: [] },
35
+ 'ClassE' => { threshold: '0', range: [] }
36
+ }.freeze
37
+
38
+ # Tooltip data:
39
+ # {
40
+ # title: '',
41
+ # description: '',
42
+ # ranks: [
43
+ # {"type":"a","year":100,"total":300},
44
+ # {"type":"b","year":80,"total":240},
45
+ # {"type":"c","year":60,"total":120},
46
+ # {"type":"d","year":40,"total":100},
47
+ # {"type":"e","year":20,"total":80}
48
+ # ]
49
+ # }
50
+ def tooltip_data(metric)
51
+ classes = CLASSES.reverse
52
+ section = Ossert::Stats.guess_section_by_metric(metric)
53
+ ranks = classes.inject([]) do |preview, klass|
54
+ base = { type: KLASS_2_GRADE[klass].downcase, year: ' N/A ', total: ' N/A ' }
55
+ preview << [:year, :total].each_with_object(base) do |section_type, result|
56
+ next unless (metric_data = metric_tooltip_data(metric, section, section_type, klass)).present?
57
+ result[section_type] = metric_data
58
+ end
59
+ end
60
+
61
+ { title: Ossert.t(metric), description: Ossert.descr(metric), ranks: ranks }
62
+ end
63
+
64
+ def metric_tooltip_data(metric, section, section_type, klass)
65
+ return if section == :not_found # this should not happen
66
+ reference_section = [section, section_type].join('_')
67
+ return unless (metric_by_grades = @reference[reference_section.to_sym][metric.to_s])
68
+
69
+ [
70
+ reversed_metrics.include?(metric) ? '&lt;&nbsp;' : '&gt;&nbsp;',
71
+ decorator.value(metric, metric_by_grades[klass][:threshold])
72
+ ].join(' ')
73
+ end
74
+
75
+ def reversed_metrics
76
+ @reversed_metrics ||= Ossert::Classifiers::Growing.config['reversed']
77
+ end
78
+
79
+ # Fast preview graph
80
+ # [
81
+ # {"title":"Jan - Mar 2016","type":"a","values":[10,20]},
82
+ # {"title":"Apr - Jun 2016","type":"b","values":[20,25]},
83
+ # {"title":"Jul - Sep 2016","type":"c","values":[25,35]},
84
+ # {"title":"Oct - Dec 2016","type":"d","values":[35,50]},
85
+ # {"title":"Next year","type":"e","values":[50,10]}
86
+ # ]
87
+ def fast_preview_graph_data(lookback = 5, check_results = nil)
88
+ graph_data = { popularity: [], maintenance: [], maturity: [] } # replace with config
89
+
90
+ check_results.each_with_index do |check_result, index|
91
+ check_result.each do |check, results|
92
+ sum_up_checks(graph_data, check, results, index, lookback - index)
93
+ end
94
+ end
95
+ graph_data.map { |k, v| [k, MultiJson.dump(v)] }.to_h
96
+ end
97
+
98
+ def sum_up_checks(graph_data, check, results, index, offset)
99
+ gain = results[:gain]
100
+ graph_data[check] << {
101
+ title: last_quarters_bounds(offset),
102
+ type: results[:mark].downcase,
103
+ values: [gain, gain]
104
+ }
105
+
106
+ graph_data[check][index - 1][:values][1] = gain if index.positive?
107
+ end
108
+
109
+ def last_quarters_bounds(last_year_offset)
110
+ date = Time.current.utc - ((last_year_offset - 1) * 3).months
111
+
112
+ [date.beginning_of_quarter.strftime('%b'),
113
+ date.end_of_quarter.strftime('%b %Y')].join(' - ')
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'ossert/presenters/project'
3
+
4
+ module Ossert
5
+ # Public: Various classes and methods for presenting data.
6
+ module Presenters
7
+ end
8
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ module Ossert
3
+ class Project
4
+ include Ossert::Saveable
5
+
6
+ attr_accessor :name, :github_alias, :rubygems_alias,
7
+ :community, :agility, :reference,
8
+ :meta, :created_at, :updated_at
9
+
10
+ META_STUB = {
11
+ homepage_url: nil,
12
+ docs_url: nil,
13
+ wiki_url: nil,
14
+ source_url: nil,
15
+ issue_tracker_url: nil,
16
+ mailing_list_url: nil,
17
+ authors: nil,
18
+ top_10_contributors: [],
19
+ description: nil,
20
+ current_version: nil,
21
+ rubygems_url: nil,
22
+ github_url: nil
23
+ }.freeze
24
+
25
+ class << self
26
+ def fetch_all(name, reference = Ossert::Saveable::UNUSED_REFERENCE)
27
+ project = find_by_name(name, reference)
28
+
29
+ Ossert::Fetch.all project
30
+ project.prepare_time_bounds!
31
+ project.dump
32
+ end
33
+
34
+ def projects_by_reference
35
+ load_referenced.group_by(&:reference)
36
+ end
37
+ end
38
+
39
+ def grade_by_growing_classifier
40
+ raise unless Classifiers::Growing.current.ready?
41
+ Classifiers::Growing.current.grade(self)
42
+ end
43
+ alias grade_by_classifier grade_by_growing_classifier
44
+
45
+ def analyze_by_decisision_tree
46
+ raise unless Classifiers::DecisionTree.current.ready?
47
+ Classifiers::DecisionTree.current.check(self)
48
+ end
49
+
50
+ def initialize(name, github_alias = nil, rubygems_alias = nil, reference = nil)
51
+ @name = name.dup
52
+ @github_alias = github_alias
53
+ @rubygems_alias = (rubygems_alias || name).dup
54
+ @reference = reference.dup
55
+
56
+ @agility = Agility.new
57
+ @community = Community.new
58
+ @meta = META_STUB.dup
59
+ end
60
+
61
+ def assign_data(meta:, agility:, community:, created_at:, updated_at:)
62
+ @agility = agility
63
+ @community = community
64
+ @meta = meta
65
+ @created_at = created_at
66
+ @updated_at = updated_at
67
+ end
68
+
69
+ def decorated
70
+ @decorated ||= Ossert::Presenters::Project.new(self)
71
+ end
72
+
73
+ TIME_BOUNDS_CONFIG = {
74
+ base_value: {
75
+ start: nil,
76
+ end: nil
77
+ },
78
+ aggregation: {
79
+ start: :min,
80
+ end: :max
81
+ },
82
+ extended: {
83
+ start: nil,
84
+ end: nil
85
+ }
86
+ }.freeze
87
+
88
+ def prepare_time_bounds!(extended_start: nil, extended_end: nil)
89
+ config = TIME_BOUNDS_CONFIG.dup
90
+ config[:base_value][:start] = Time.now.utc
91
+ config[:base_value][:end] = 20.years.ago
92
+ config[:extended][:start] = (extended_start || Time.now.utc).to_datetime
93
+ config[:extended][:end] = (extended_end || 20.years.ago).to_datetime
94
+
95
+ agility.quarters.fullfill! && community.quarters.fullfill!
96
+
97
+ [:start, :end].map { |time_bound| time_bound_values(time_bound, config).to_date }
98
+ end
99
+
100
+ def time_bound_values(time_bound, config)
101
+ [
102
+ config[:base_value][time_bound], config[:extended][time_bound],
103
+ agility.quarters.send("#{time_bound}_date"), community.quarters.send("#{time_bound}_date")
104
+ ].send(config[:aggregation][time_bound])
105
+ end
106
+
107
+ def meta_to_json
108
+ MultiJson.dump(meta)
109
+ end
110
+
111
+ class BaseStore
112
+ attr_accessor :quarters, :total, :total_prediction, :quarter_prediction
113
+
114
+ def initialize(quarters: nil, total: nil)
115
+ @quarters = quarters || QuartersStore.new(self.class.quarter_stats_klass_name)
116
+ @total = total || ::Kernel.const_get(self.class.total_stats_klass_name).new
117
+ end
118
+ end
119
+
120
+ class Agility < BaseStore
121
+ class << self
122
+ def quarter_stats_klass_name
123
+ 'Ossert::Stats::AgilityQuarter'
124
+ end
125
+
126
+ def total_stats_klass_name
127
+ 'Ossert::Stats::AgilityTotal'
128
+ end
129
+ end
130
+ end
131
+
132
+ class Community < BaseStore
133
+ class << self
134
+ def quarter_stats_klass_name
135
+ 'Ossert::Stats::CommunityQuarter'
136
+ end
137
+
138
+ def total_stats_klass_name
139
+ 'Ossert::Stats::CommunityTotal'
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end