snowpack 1.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +16 -0
  6. data/Brewfile +2 -0
  7. data/CHANGELOG.md +25 -0
  8. data/Gemfile +13 -0
  9. data/LICENSE +21 -0
  10. data/README.md +116 -0
  11. data/Rakefile +6 -0
  12. data/bin/bootstrap +25 -0
  13. data/exe/snowpack +7 -0
  14. data/lib/snowpack/application.rb +98 -0
  15. data/lib/snowpack/application_cli.rb +39 -0
  16. data/lib/snowpack/cli/application/cli.rb +32 -0
  17. data/lib/snowpack/cli/application/command.rb +53 -0
  18. data/lib/snowpack/cli/application/commands/assets/clobber.rb +31 -0
  19. data/lib/snowpack/cli/application/commands/assets/compile.rb +32 -0
  20. data/lib/snowpack/cli/application/commands/console.rb +55 -0
  21. data/lib/snowpack/cli/application/commands/db/create.rb +33 -0
  22. data/lib/snowpack/cli/application/commands/db/create_migration.rb +38 -0
  23. data/lib/snowpack/cli/application/commands/db/drop.rb +33 -0
  24. data/lib/snowpack/cli/application/commands/db/migrate.rb +47 -0
  25. data/lib/snowpack/cli/application/commands/db/reset.rb +28 -0
  26. data/lib/snowpack/cli/application/commands/db/rollback.rb +54 -0
  27. data/lib/snowpack/cli/application/commands/db/sample_data.rb +41 -0
  28. data/lib/snowpack/cli/application/commands/db/seed.rb +41 -0
  29. data/lib/snowpack/cli/application/commands/db/structure/dump.rb +38 -0
  30. data/lib/snowpack/cli/application/commands/db/utils/database.rb +76 -0
  31. data/lib/snowpack/cli/application/commands/db/utils/database_config.rb +49 -0
  32. data/lib/snowpack/cli/application/commands/db/version.rb +37 -0
  33. data/lib/snowpack/cli/application/commands/generate/slice.rb +32 -0
  34. data/lib/snowpack/cli/application/commands/routes/update.rb +35 -0
  35. data/lib/snowpack/cli/application/commands.rb +27 -0
  36. data/lib/snowpack/cli/command.rb +48 -0
  37. data/lib/snowpack/cli/standalone/cli.rb +14 -0
  38. data/lib/snowpack/cli/standalone/commands/new.rb +33 -0
  39. data/lib/snowpack/cli/standalone/commands.rb +13 -0
  40. data/lib/snowpack/components/formalist.rb +27 -0
  41. data/lib/snowpack/components/persistence.rb +56 -0
  42. data/lib/snowpack/components.rb +5 -0
  43. data/lib/snowpack/console/context.rb +32 -0
  44. data/lib/snowpack/console/plugins/relation_readers.rb +17 -0
  45. data/lib/snowpack/console/plugins/slice_readers.rb +33 -0
  46. data/lib/snowpack/generator.rb +57 -0
  47. data/lib/snowpack/generators/application/generator.rb +67 -0
  48. data/lib/snowpack/generators/application/templates/.env-example.tt +10 -0
  49. data/lib/snowpack/generators/application/templates/.env.test-example.tt +1 -0
  50. data/lib/snowpack/generators/application/templates/.gitignore +20 -0
  51. data/lib/snowpack/generators/application/templates/.rspec +2 -0
  52. data/lib/snowpack/generators/application/templates/Brewfile +4 -0
  53. data/lib/snowpack/generators/application/templates/Gemfile.tt +46 -0
  54. data/lib/snowpack/generators/application/templates/Guardfile +7 -0
  55. data/lib/snowpack/generators/application/templates/Procfile.dev +3 -0
  56. data/lib/snowpack/generators/application/templates/Procfile.support +1 -0
  57. data/lib/snowpack/generators/application/templates/README.md.tt +45 -0
  58. data/lib/snowpack/generators/application/templates/Rakefile +5 -0
  59. data/lib/snowpack/generators/application/templates/bin/run +7 -0
  60. data/lib/snowpack/generators/application/templates/config/application.rb.tt +19 -0
  61. data/lib/snowpack/generators/application/templates/config/puma.rb.tt +14 -0
  62. data/lib/snowpack/generators/application/templates/config/routes.rb.tt +15 -0
  63. data/lib/snowpack/generators/application/templates/config.ru +5 -0
  64. data/lib/snowpack/generators/application/templates/db/migrate/.keep +0 -0
  65. data/lib/snowpack/generators/application/templates/db/sample_data.rb +1 -0
  66. data/lib/snowpack/generators/application/templates/db/seed.rb +1 -0
  67. data/lib/snowpack/generators/application/templates/lib/__application_path__/operation.rb.tt +12 -0
  68. data/lib/snowpack/generators/application/templates/lib/__application_path__/persistence/relations/.keep +0 -0
  69. data/lib/snowpack/generators/application/templates/lib/__application_path__/types.rb.tt +11 -0
  70. data/lib/snowpack/generators/application/templates/lib/__application_path__/web/action.rb.tt +56 -0
  71. data/lib/snowpack/generators/application/templates/lib/__application_path__/web/view/context.rb.tt +31 -0
  72. data/lib/snowpack/generators/application/templates/lib/hanami/action/csrf_protection.rb +225 -0
  73. data/lib/snowpack/generators/application/templates/log/.keep +0 -0
  74. data/lib/snowpack/generators/application/templates/package.json.tt +25 -0
  75. data/lib/snowpack/generators/application/templates/public/.keep +0 -0
  76. data/lib/snowpack/generators/application/templates/script/bootstrap +32 -0
  77. data/lib/snowpack/generators/application/templates/script/console +26 -0
  78. data/lib/snowpack/generators/application/templates/script/server +13 -0
  79. data/lib/snowpack/generators/application/templates/script/setup +27 -0
  80. data/lib/snowpack/generators/application/templates/script/support +9 -0
  81. data/lib/snowpack/generators/application/templates/script/test +19 -0
  82. data/lib/snowpack/generators/application/templates/script/update +13 -0
  83. data/lib/snowpack/generators/application/templates/spec/spec_helper.rb +13 -0
  84. data/lib/snowpack/generators/application/templates/spec/suite/.keep +0 -0
  85. data/lib/snowpack/generators/application/templates/spec/support/suite.rb +9 -0
  86. data/lib/snowpack/generators/application/templates/system/__application_path__/import.rb.tt +5 -0
  87. data/lib/snowpack/generators/application/templates/system/boot/assets.rb.tt +17 -0
  88. data/lib/snowpack/generators/application/templates/system/boot/logger.rb.tt +10 -0
  89. data/lib/snowpack/generators/application/templates/system/boot/monitor.rb.tt +9 -0
  90. data/lib/snowpack/generators/application/templates/system/boot/persistence.rb.tt +9 -0
  91. data/lib/snowpack/generators/application/templates/system/boot/settings.rb.tt +18 -0
  92. data/lib/snowpack/generators/application/templates/system/boot/web.rb.tt +9 -0
  93. data/lib/snowpack/generators/slice/generator.rb +62 -0
  94. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/.keep +0 -0
  95. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/action.rb.tt +10 -0
  96. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/actions/.keep +0 -0
  97. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/view.rb.tt +17 -0
  98. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/views/.keep +0 -0
  99. data/lib/snowpack/generators/slice/templates/system/__slice_path__/import.rb.tt +9 -0
  100. data/lib/snowpack/generators/slice/templates/system/__slice_path__/slice.rb.tt +10 -0
  101. data/lib/snowpack/instrumentation/appsignal/appsignal_ext.rb +13 -0
  102. data/lib/snowpack/instrumentation/appsignal/que.rb +70 -0
  103. data/lib/snowpack/instrumentation/appsignal/rack.rb +84 -0
  104. data/lib/snowpack/roda/web.rb +47 -0
  105. data/lib/snowpack/slice.rb +85 -0
  106. data/lib/snowpack/test/suite.rb +164 -0
  107. data/lib/snowpack/test/tasks.rake +40 -0
  108. data/lib/snowpack/test_tasks.rb +4 -0
  109. data/lib/snowpack/types.rb +9 -0
  110. data/lib/snowpack/version.rb +5 -0
  111. data/lib/snowpack/view/part_builder.rb +45 -0
  112. data/lib/snowpack/view/parts/pager.rb +107 -0
  113. data/lib/snowpack/view/parts/paginated.rb +22 -0
  114. data/lib/snowpack/web/application.rb +38 -0
  115. data/lib/snowpack/web/assets.rb +56 -0
  116. data/lib/snowpack/web/endpoint_resolver.rb +66 -0
  117. data/lib/snowpack/web/form.rb +38 -0
  118. data/lib/snowpack/web/plugin.rb +44 -0
  119. data/lib/snowpack/web/rack_logger.rb +73 -0
  120. data/lib/snowpack/web/router.rb +58 -0
  121. data/lib/snowpack/web.rb +20 -0
  122. data/lib/snowpack.rb +27 -0
  123. data/snowpack.gemspec +34 -0
  124. metadata +327 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal"
4
+ require_relative "appsignal_ext"
5
+ require "securerandom"
6
+
7
+ # Remove Appsignal's own Que integration hook, so the Snowpack-hosted
8
+ # integration can be included manually.
9
+ Appsignal::Hooks.hooks.delete :que
10
+
11
+ module Snowpack
12
+ module Instrumentation
13
+ module Appsignal
14
+ module Que
15
+ # Enable like so:
16
+ #
17
+ # Que.error_notifier = Snowpack::Instrumentation::Appsignal::Que::ErrorNotifier
18
+
19
+ # Actually, why is this even necessry?
20
+ ErrorNotifier = -> error, _job {
21
+ ::Appsignal::Transaction.current.set_error(error)
22
+ }
23
+
24
+ def self.included(base)
25
+ base.class_eval do
26
+ def _run_with_appsignal(*args)
27
+ env = {
28
+ :metadata => {
29
+ :id => attrs[:job_id],
30
+ :queue => attrs[:queue],
31
+ :run_at => attrs[:run_at].to_s,
32
+ :priority => attrs[:priority],
33
+ :attempts => attrs[:error_count].to_i
34
+ },
35
+ :params => attrs[:args]
36
+ }
37
+
38
+ request = ::Appsignal::Transaction::GenericRequest.new(env)
39
+
40
+ transaction = ::Appsignal::Transaction.create(
41
+ SecureRandom.uuid,
42
+ ::Appsignal::Transaction::BACKGROUND_JOB,
43
+ request,
44
+ )
45
+
46
+ transaction.discard! unless instrument?
47
+
48
+ begin
49
+ ::Appsignal.instrument("perform_job.que") { _run_without_appsignal(*args) }
50
+ rescue Exception => error
51
+ transaction.set_error(error)
52
+ raise error
53
+ ensure
54
+ transaction.set_action "#{attrs[:job_class]}#run"
55
+ ::Appsignal::Transaction.complete_current!
56
+ end
57
+ end
58
+
59
+ alias_method :_run_without_appsignal, :_run
60
+ alias_method :_run, :_run_with_appsignal
61
+
62
+ def instrument?
63
+ true
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # use Snowpack::Instrumentation::Rack::Appsignal, {
4
+ # sample: 0.2,
5
+ # filters: {
6
+ # "/some/exact/route" => 0.05, # <- custom sample rate (5%) for specific URL
7
+ # %r{/another/.+/route} => false, # <- do not instrument this URL, n.b. match via Regexp
8
+ # },
9
+ # }
10
+
11
+ require "appsignal"
12
+ require_relative "appsignal_ext"
13
+ require "rack"
14
+ require "securerandom"
15
+
16
+ module Snowpack
17
+ module Instrumentation
18
+ module Appsignal
19
+ class Rack
20
+ attr_reader :app, :options
21
+
22
+ def initialize(app, **options)
23
+ @app = app
24
+ @options = options
25
+ end
26
+
27
+ def call(env)
28
+ if ::Appsignal.active?
29
+ call_with_appsignal_monitoring(env)
30
+ else
31
+ app.call(env)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def call_with_appsignal_monitoring(env)
38
+ request = ::Rack::Request.new(env)
39
+
40
+ transaction = ::Appsignal::Transaction.create(
41
+ SecureRandom.uuid,
42
+ ::Appsignal::Transaction::HTTP_REQUEST,
43
+ request,
44
+ )
45
+
46
+ transaction.discard! unless sample?(request)
47
+
48
+ begin
49
+ ::Appsignal.instrument "process_action.generic" do
50
+ app.call(env)
51
+ end
52
+ rescue Exception => error
53
+ transaction.set_error(error)
54
+ raise error
55
+ ensure
56
+ if env["appsignal.route"]
57
+ transaction.set_action_if_nil(env["appsignal.route"])
58
+ else
59
+ transaction.set_action_if_nil("unknown")
60
+ end
61
+
62
+ transaction.set_metadata("path", request.path)
63
+ transaction.set_metadata("method", request.request_method)
64
+
65
+ transaction.set_http_or_background_queue_start
66
+ ::Appsignal::Transaction.complete_current!
67
+ end
68
+ end
69
+
70
+ def sample?(request)
71
+ rate = options.fetch(:sample, 1)
72
+
73
+ options.fetch(:filters, {}).each do |match, sample|
74
+ rate = sample and break if request.path.match?(match)
75
+ end
76
+
77
+ rate = 1 if rate == true
78
+
79
+ rate && rand <= rate
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO wrap in begin/rescue loaderror and raise a meaningful exception
4
+ require "roda"
5
+ require "roda/plugins/flow"
6
+
7
+ module Snowpack
8
+ module Roda
9
+ # Mostly copied from Dry::Web::Roda::Application
10
+ class Web < ::Roda
11
+ extend Dry::Configurable
12
+
13
+ setting :container, reader: true
14
+ setting :routes
15
+
16
+ plugin :flow
17
+
18
+ # Disable this so we can better control when it's used
19
+ #
20
+ # def self.configure(&block)
21
+ # super.tap do
22
+ # use(container[:rack_monitor]) if container.config.listeners
23
+ # end
24
+ # end
25
+
26
+ def self.resolve(name)
27
+ container[name]
28
+ end
29
+
30
+ def self.[](name)
31
+ container[name]
32
+ end
33
+
34
+ def self.load_routes!
35
+ Dir[root.join("#{config.routes}/**/*.rb")].each { |f| require f }
36
+ end
37
+
38
+ def self.root
39
+ container.config.root
40
+ end
41
+
42
+ # def notifications
43
+ # self.class[:notifications]
44
+ # end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../snowpack"
5
+
6
+ module Snowpack
7
+ class Slice < Dry::System::Container
8
+ use :env, inferrer: -> { ENV.fetch('RACK_ENV', 'development').to_sym }
9
+
10
+ def self.inherited(klass)
11
+ super
12
+
13
+ if klass.superclass == Snowpack::Slice
14
+ raise "Snowpack.application not configured yet" unless Snowpack.application?
15
+
16
+ app = Snowpack.application
17
+
18
+ klass.instance_variable_set :@application, app
19
+
20
+ klass.config.env = app.env
21
+ klass.config.name = klass.slice_name.to_sym
22
+ klass.config.auto_register = [File.join("lib", klass.slice_namespace_path)]
23
+ klass.config.default_namespace = klass.slice_namespace_identifier_prefix
24
+
25
+ slice_path = File.join(app.config.root, app.config.slices_dir, klass.slice_name)
26
+ if File.directory?(slice_path)
27
+ klass.config.root = slice_path if File.directory?(slice_path)
28
+ klass.load_paths! "lib"
29
+ end
30
+
31
+ klass.import application: app
32
+ end
33
+ end
34
+
35
+ def self.import_slice(*slices)
36
+ @slice_imports ||= []
37
+ @slice_imports += slices
38
+ @slice_imports.uniq!
39
+ self
40
+ end
41
+
42
+ def self.application
43
+ @application
44
+ end
45
+
46
+ def self.boot!
47
+ finalize!
48
+ end
49
+
50
+ def self.finalize!
51
+ # Force `after :configure` hooks to run
52
+ configure do; end
53
+
54
+ super do
55
+ Array(@slice_imports).each do |slice|
56
+ import slice => application.slices.fetch(slice)
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ MODULE_DELIMITER = "::"
64
+
65
+ def self.slice_namespace
66
+ inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
67
+ end
68
+
69
+ def self.slice_namespace_path
70
+ inflector.underscore(slice_namespace.to_s)
71
+ end
72
+
73
+ def self.slice_namespace_identifier_prefix
74
+ slice_namespace_path.gsub("/", ".")
75
+ end
76
+
77
+ def self.slice_name
78
+ inflector.underscore(slice_namespace.to_s.split(MODULE_DELIMITER).last)
79
+ end
80
+
81
+ def self.inflector
82
+ application[:inflector]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'json'
5
+
6
+ require_relative "../../snowpack"
7
+
8
+ module Snowpack
9
+ module Test
10
+ module Helpers
11
+ module_function
12
+
13
+ def suite
14
+ @__suite__ ||= RSpec.configuration.suite
15
+ end
16
+ end
17
+
18
+ class Suite
19
+ DB_CLEANUP_PATH_REGEX = /(features|integration)/
20
+
21
+ class << self
22
+ attr_accessor :current
23
+
24
+ def root
25
+ @__root__ ||= Pathname(Dir.pwd).join("spec").freeze
26
+ end
27
+
28
+ def instance
29
+ @__instance__ ||= new
30
+ end
31
+ end
32
+
33
+ def self.inherited(klass)
34
+ super
35
+ Suite.current = klass
36
+ end
37
+
38
+ def self.configure
39
+ RSpec.configure do |config|
40
+ config.add_setting :suite
41
+
42
+ config.suite = self.new
43
+
44
+ config.include Helpers
45
+
46
+ config.disable_monkey_patching!
47
+
48
+ config.expect_with :rspec do |expectations|
49
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
50
+ end
51
+
52
+ config.mock_with :rspec do |mocks|
53
+ mocks.verify_partial_doubles = true
54
+ end
55
+
56
+ config.filter_run :focus
57
+ config.run_all_when_everything_filtered = true
58
+
59
+ config.example_status_persistence_file_path = root.join("../tmp").realpath.join("spec/examples.txt").to_s
60
+
61
+ if config.files_to_run.one?
62
+ config.default_formatter = "doc"
63
+ end
64
+
65
+ config.profile_examples = 10
66
+
67
+ config.order = :random
68
+
69
+ Kernel.srand config.seed
70
+
71
+ yield(config) if block_given?
72
+ end
73
+ end
74
+
75
+ attr_reader :application, :root
76
+
77
+ def initialize(application: Snowpack.application, root: self.class.root.join("suite"))
78
+ @application = application
79
+ @root = root
80
+
81
+ application.load_slices
82
+ end
83
+
84
+ def start_coverage
85
+ return unless coverage?
86
+
87
+ require 'simplecov'
88
+
89
+ if parallel?
90
+ SimpleCov.command_name(test_group_name)
91
+ end
92
+
93
+ SimpleCov.start do
94
+ add_filter '/spec/'
95
+ add_filter '/system/'
96
+ end
97
+ end
98
+
99
+ def coverage_threshold
100
+ ENV.fetch('COVERAGE_THRESHOLD').to_f.round
101
+ end
102
+
103
+ def current_coverage
104
+ data = JSON.load(application.root.join('coverage/.last_run.json'))
105
+ data.fetch('result').fetch('covered_percent').to_f.round
106
+ end
107
+
108
+ def test_group_name
109
+ @__test_group_name__ ||= "test_suite_#{build_idx}"
110
+ end
111
+
112
+ def chdir(name)
113
+ self.class.new(
114
+ application: application,
115
+ root: root.join(name.to_s),
116
+ )
117
+ end
118
+
119
+ def files
120
+ dirs.map { |dir| dir.join("**/*_spec.rb") }.flat_map { |path| Dir[path] }.sort
121
+ end
122
+
123
+ def groups
124
+ dirs.map(&:basename).map(&:to_s).map(&:to_sym).sort
125
+ end
126
+
127
+ def dirs
128
+ Dir[root.join("*")].map(&Kernel.method(:Pathname)).select(&:directory?)
129
+ end
130
+
131
+ def build_idx
132
+ ENV.fetch('CIRCLE_NODE_INDEX', -1).to_i
133
+ end
134
+
135
+ def coverage?
136
+ ENV['COVERAGE'] == 'true'
137
+ end
138
+
139
+ def ci?
140
+ !ENV['CIRCLECI'].nil?
141
+ end
142
+
143
+ def parallel?
144
+ ENV['CIRCLE_NODE_TOTAL'].to_i > 1
145
+ end
146
+
147
+ def capybara_server_port
148
+ 3001
149
+ end
150
+
151
+ def log_dir
152
+ application.root.join("log").to_s
153
+ end
154
+
155
+ def tmp_dir
156
+ application.root.join("tmp").to_s
157
+ end
158
+
159
+ def clean_db?(example)
160
+ DB_CLEANUP_PATH_REGEX.match?(example.metadata[:file_path])
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,40 @@
1
+ begin
2
+ require "rspec/core/rake_task"
3
+ require "snowpack/test/suite"
4
+
5
+ require Snowpack::Test::Suite.root.join("support/suite")
6
+ suite = Snowpack::Test::Suite.current.new
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new :spec do |t|
10
+ opts = ["#{suite.root} --pattern **/*_spec.rb"]
11
+
12
+ if suite.ci?
13
+ opts << "--format RspecJunitFormatter"
14
+ opts << "--out /tmp/test-results/rspec.xml"
15
+ opts << "--format progress"
16
+ end
17
+
18
+ t.rspec_opts = opts.join(" ")
19
+ end
20
+
21
+ namespace :spec do
22
+ suite.groups.each do |group|
23
+ desc "Run #{group} specs"
24
+ RSpec::Core::RakeTask.new(group) do |t|
25
+ t.rspec_opts = "#{suite.chdir(group).root} --pattern **/*_spec.rb"
26
+ end
27
+ end
28
+
29
+ desc 'Verify coverage'
30
+ task :verify_coverage do
31
+ if suite.current_coverage.to_i < suite.coverage_threshold
32
+ puts "Coverage too low. Current: #{suite.current_coverage}%; Expected min: #{suite.coverage_threshold}%"
33
+ exit 1
34
+ end
35
+ end
36
+ end
37
+
38
+ task default: [:spec]
39
+ rescue LoadError; end
40
+ # FIXME ^ make that way more specific
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ load "snowpack/test/tasks.rake"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+
5
+ module Snowpack
6
+ module Types
7
+ include Dry.Types(default: :strict)
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowpack
4
+ VERSION = "1.0.0.alpha2"
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part_builder"
4
+ require "snowpack/view/parts/pager"
5
+ require_relative "parts/paginated"
6
+
7
+ module Snowpack
8
+ module View
9
+ class PartBuilder < Dry::View::PartBuilder
10
+ def call(name, value, **options)
11
+ if paginated_collection?(value)
12
+ build_paginated_collection_part(name, value, **options)
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def paginated_collection?(value)
21
+ value.respond_to?(:to_a) && value.respond_to?(:pager)
22
+ end
23
+
24
+ def build_paginated_collection_part(name, value, **options)
25
+ pager_part = build_part(:pager, value.pager, as: Snowpack::View::Parts::Pager)
26
+
27
+ collection_as = collection_options(name: name, **options)[:as]
28
+ item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)
29
+
30
+ arr = value.to_ary.map { |obj|
31
+ call(item_name, obj, **options.merge(as: item_as))
32
+ }
33
+
34
+ collection_part_class = part_class(name: name, fallback_class: Parts::Paginated, **options.merge(as: collection_as))
35
+
36
+ collection_part_class.new(
37
+ name: name,
38
+ value: arr,
39
+ pager: pager_part,
40
+ render_env: render_env,
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part"
4
+ require "rack/utils"
5
+ require "uri"
6
+
7
+ module Snowpack
8
+ module View
9
+ module Parts
10
+ class Pager < Dry::View::Part
11
+ attr_reader :part_args
12
+ attr_reader :buffer
13
+
14
+ def initialize(buffer: 3, **part_args)
15
+ @part_args = part_args
16
+ @buffer = buffer
17
+
18
+ super(**part_args)
19
+ end
20
+
21
+ def with(**options)
22
+ new_options = {
23
+ buffer: buffer
24
+ }.merge(options)
25
+
26
+ self.class.new(**new_options, **part_args)
27
+ end
28
+
29
+ def first_page
30
+ 1
31
+ end
32
+
33
+ def first_page?
34
+ current_page == first_page
35
+ end
36
+
37
+ def last_page
38
+ total_pages
39
+ end
40
+
41
+ def last_page?
42
+ current_page == last_page
43
+ end
44
+
45
+ def prev_gap?
46
+ current_page - buffer > first_page
47
+ end
48
+
49
+ def next_gap?
50
+ current_page + buffer < last_page
51
+ end
52
+
53
+ def single_page?
54
+ total_pages <= 1
55
+ end
56
+
57
+ def all_page_numbers
58
+ (first_page..last_page).to_a
59
+ end
60
+
61
+ def each_entry
62
+ return to_enum(:each_entry) unless block_given?
63
+
64
+ entry_numbers.each do |number|
65
+ yield Entry.new(number, current: number == current_page)
66
+ end
67
+ end
68
+
69
+ def url_for_page(page)
70
+ uri = URI(_context.fullpath)
71
+ params = Rack::Utils.parse_query(uri.query).merge("page" => page.to_s)
72
+ uri.query = Rack::Utils.build_query(params)
73
+ uri.to_s
74
+ end
75
+
76
+ private
77
+
78
+ def entry_numbers
79
+ left_edge = current_page - buffer
80
+ left_edge = 1 if left_edge < 1
81
+
82
+ right_edge = current_page + buffer
83
+ right_edge = total_pages if right_edge > total_pages
84
+
85
+ left_edge.upto(right_edge).to_a
86
+ end
87
+
88
+ class Entry
89
+ attr_reader :number
90
+
91
+ def initialize(number, current:)
92
+ @number = number
93
+ @current = current
94
+ end
95
+
96
+ def to_s
97
+ number.to_s
98
+ end
99
+
100
+ def current?
101
+ !!@current
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part"
4
+
5
+ module Snowpack
6
+ module View
7
+ module Parts
8
+ class Paginated < Dry::View::Part
9
+ attr_reader :pager
10
+
11
+ def initialize(pager:, **part_args)
12
+ @pager = pager
13
+ super(**part_args)
14
+ end
15
+
16
+ def pagination(locals: {}, **options)
17
+ pager.with(**options).render(:pagination, **locals)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end