adalog 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/Gemfile +3 -0
- data/LICENSE.md +27 -0
- data/README.md +144 -0
- data/Rakefile +7 -0
- data/adalog.gemspec +27 -0
- data/lib/adalog/active_record_repo.rb +62 -0
- data/lib/adalog/configuration.rb +46 -0
- data/lib/adalog/entry.rb +85 -0
- data/lib/adalog/in_memory_repo.rb +38 -0
- data/lib/adalog/pstore_repo.rb +62 -0
- data/lib/adalog/simple_logging_adapter.rb +21 -0
- data/lib/adalog/version.rb +3 -0
- data/lib/adalog/web/public/javascripts/adalog.js +2 -0
- data/lib/adalog/web/public/javascripts/entries-list.js +82 -0
- data/lib/adalog/web/public/javascripts/vanilla-js.js +103 -0
- data/lib/adalog/web/public/stylesheets/adalog.css +55 -0
- data/lib/adalog/web/public/stylesheets/entries-list.css +99 -0
- data/lib/adalog/web/public/stylesheets/pure/buttons-min.css +7 -0
- data/lib/adalog/web/public/stylesheets/pure/forms-min.css +7 -0
- data/lib/adalog/web/public/stylesheets/pure/menus-min.css +7 -0
- data/lib/adalog/web/public/stylesheets/pure/pure-base-min.css +11 -0
- data/lib/adalog/web/public/stylesheets/pure/pure-grids-responsive-min.css +7 -0
- data/lib/adalog/web/public/stylesheets/pure/tables-min.css +7 -0
- data/lib/adalog/web/views/adalog.html.erb +24 -0
- data/lib/adalog/web/views/index.html.erb +37 -0
- data/lib/adalog/web.rb +103 -0
- data/lib/adalog.rb +60 -0
- data/test/data/simple.yml +28 -0
- data/test/test_helper.rb +35 -0
- data/test/unit/active_record_repo.rb +9 -0
- data/test/unit/configuration_test.rb +62 -0
- data/test/unit/entry_test.rb +138 -0
- data/test/unit/in_memory_repo_test.rb +9 -0
- data/test/unit/pstore_repo_test.rb +9 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c15c023b9348c277ec76330788aa48ff50e97009
|
4
|
+
data.tar.gz: 1a67e84184ec901ae73f58253decf22e0196e8b5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c80d618c9f584f39167f2ef97b42ff1f05eddb0c6b8f0dab7664d21bd8183e05bdb471baeff58627db214f48e38e3a94c81b12eea0f15c3e664d2c6e15677e38
|
7
|
+
data.tar.gz: 583ba7212ab7caf2fda0bd7772bbee8af0f7a9b7f59c9ea31c21305593fab93e5f46585e34dd3df75ff9b11fd1b30fa65510d2b3fe567ba78c96af800829c573
|
data/.gitignore
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
.DS_Store
|
2
|
+
lob/*.log
|
3
|
+
log/*.log
|
4
|
+
tmp/**/*
|
5
|
+
*.swp
|
6
|
+
*.tmproj
|
7
|
+
tmtags
|
8
|
+
.project
|
9
|
+
.bundle
|
10
|
+
*.sublime-project
|
11
|
+
*.sublime-workspace
|
12
|
+
.vagrant
|
13
|
+
.ruby-version
|
14
|
+
.ruby-gemset
|
15
|
+
*.gem
|
16
|
+
*.rbc
|
17
|
+
.config
|
18
|
+
.yardoc
|
19
|
+
InstalledFiles
|
20
|
+
Gemfile.lock
|
21
|
+
_yardoc
|
22
|
+
coverage
|
23
|
+
doc/
|
24
|
+
lib/bundler/man
|
25
|
+
pkg
|
26
|
+
rdoc
|
27
|
+
test/tmp
|
28
|
+
test/version_tmp
|
29
|
+
config.ru
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2015, Paul Kwiatkowski
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
* Neither the name of adalog nor the names of its
|
15
|
+
contributors may be used to endorse or promote products derived from
|
16
|
+
this software without specific prior written permission.
|
17
|
+
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Adalog
|
2
|
+
|
3
|
+
It pairs some Log-like Repository implementation with a Sinatra app. What an achievement, right? No? I agree, but read on.
|
4
|
+
|
5
|
+
|
6
|
+
## Motivation
|
7
|
+
|
8
|
+
Far, far too many third party services do not bother to implement competent sandbox environments. If you're doing "the right thing" and wrapping their client libraries in your own adapter layer (of some variety), you can easily swap out for a stub implementation in development, test or staging environments.
|
9
|
+
|
10
|
+
Great! But now you have to write a second set test of adapaters yourself. So... not great! And sometimes all you want to do is check they were even sending (or receiving) the right message/data anyway?
|
11
|
+
|
12
|
+
In the way that Matz "is not a concurrency guy", I "am not a logging guy". If I'm poking around in a UI while developing, I don't want to cram even more into my logs just to make sure an API call with big chunk of JSON got sent off properly, amidst 30 render statements, 3 or 4 SQL queries, and probably some stuff going in to/out of Redis.
|
13
|
+
|
14
|
+
In fact, since I'm already building a webpage, why can't I see it in my browser?
|
15
|
+
|
16
|
+
Enter <strong>Log</strong>ging for Stub <strong>Ada</strong>pters: Adalog
|
17
|
+
|
18
|
+
|
19
|
+
## We _Can_ Have Nice Things
|
20
|
+
|
21
|
+
Adalog really only needs one thing: A repository to put entries into and take them out of. For this purpose, a repository (repo for short) is anything that responds to four messages: `fetch`, `insert`, `clear!`, and `all`.
|
22
|
+
|
23
|
+
Adalog even comes with three available repos: `InMemoryRepo`, `PStoreRepo` and `ActiveRecordRepo` which all do exactly what their names suggest.
|
24
|
+
|
25
|
+
The Sinatra app has (for now) a single page that lists the output of `Adalog.configuration.repo.all` in reverse chronological order. Ideal for running as its own little service or mounting in a Rails app during in the development environment!
|
26
|
+
|
27
|
+
## Example Configuration and Usage
|
28
|
+
|
29
|
+
After the usual addition of `gem 'adalog'` to your `Gemfile` and `bundle install` usage can be achieved by the following:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
Adalog.configure do |config|
|
33
|
+
# No action needed, however the default repo is a non-threadsafe InMemoryRepo
|
34
|
+
end
|
35
|
+
|
36
|
+
# Insert something that has a 'title', optionally a 'message' and 'details'
|
37
|
+
Adalog.configuration.repo.insert(
|
38
|
+
title: "StubSendGridAdapter",
|
39
|
+
message: "Email Sent")
|
40
|
+
```
|
41
|
+
|
42
|
+
With only one repo in use, Adalog will forward the messages of `insert`, `fetch`, `clear!` and `all` to that one repo. Like so:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
Adalog.insert(
|
46
|
+
title: "StubSendGridAdapter",
|
47
|
+
message: "Email Sent"
|
48
|
+
details: {
|
49
|
+
to: "foo@example.com",
|
50
|
+
from: "bar@example.com",
|
51
|
+
template: "welcome-email"
|
52
|
+
})
|
53
|
+
```
|
54
|
+
|
55
|
+
The default repos also accept entries with a `timestamp`, which defaults to `Time.now` and a `format` of the `details` attribute to assist in displaying in the Sinatra app's view. The `format` defaults to `'json'`.
|
56
|
+
|
57
|
+
Speaking of the app, it can be executed in the usual manner of creating a `config.ru` file:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require 'adalog'
|
61
|
+
run Adalog::Web
|
62
|
+
```
|
63
|
+
|
64
|
+
followed by `rackup` in a terminal. Or, within a Rails app, it can be mounted via `routes.rb` at a particular path:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
YourApp::Application.routes.draw do
|
68
|
+
|
69
|
+
unless Rails.env.production?
|
70
|
+
mount Adalog::Web => '/adalog'
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Visit the particular URL for the Sinatra standalone or Rails route, and _voilà!_
|
77
|
+
|
78
|
+
|
79
|
+
## Configuration Options
|
80
|
+
|
81
|
+
Actually, that _"voilà!"_ might have been a little underwhelming. The default `InMemoryRepo` is blank upon each boot (it is in-memory, after all) so the default configuration won't show anything initially. Specifying a different repository will fix that. It is one configuration option among the following:
|
82
|
+
|
83
|
+
- **repo:** The repository to store and retreive from. Defaults to `Adalog::InMemoryRepo`.
|
84
|
+
- **singleton:** Whether or not to add class methods to the root `Adalog` module to access a singular repo. Defaults to `true`.
|
85
|
+
- **time_format:** the `strftime` format string to use when displaying dates in the app's views. Defaults to `"%H:%M:%S - %d %b %Y"`.
|
86
|
+
- **web_heading:** the title of the heading in the app's views.
|
87
|
+
- **erb_layout:** _(not yet implemented)_ set the layout for the app's views.
|
88
|
+
- **views_folder:** _(not yet implemented)_ specify a custom views folder for the Sinatra app if you feel like rolling your own entirely new version of the front-end.
|
89
|
+
|
90
|
+
As an example, here is a configuration that uses a threadsafe PStore to persist the log entries, changes the time formatting, and sets a custom heading:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
Adalog.configure do |config|
|
94
|
+
pstore_file = "log/adalog-#{ENV['RACK_ENV']}.pstore"
|
95
|
+
config.repo = Adalog::PStoreAdapter.new(pstore_file)
|
96
|
+
config.time_format = "%b %d %H:%M:%S"
|
97
|
+
config.web_heading = "Poor Richard's Third Party API Calls"
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
And now if, in the course of building out your application, you can make an adapter which simply calls out to `Adalog.insert` with its data rather than a non-sandboxed third-party service, and you can validate "success" (insofar as your local code succeeded) by visiting a web page instead of checking a logfile!
|
102
|
+
|
103
|
+
|
104
|
+
## Included Adapters
|
105
|
+
|
106
|
+
Much like how useful repositories come built-in, there are also basic adapters included in the project. Presently the only included adapter that saves you from writing your own, is `SimpleLoggingAdapter`, which uses `method_missing` to write entries for every method call made on it. For example:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# After configuring Adalog
|
110
|
+
adapter = Adalog::SimpleLoggingAdapter.new("StubMoosendAdapter", Adalog.repo)
|
111
|
+
adapter.unsubscribe_email('baz@example.com')
|
112
|
+
```
|
113
|
+
|
114
|
+
And now `Adalog.repo` contains the entry:
|
115
|
+
```ruby
|
116
|
+
{ title: "StubMoosendAdapter",
|
117
|
+
timestamp: "...",
|
118
|
+
message: "unsubscribe_email",
|
119
|
+
details: "['baz@example.com']",
|
120
|
+
format: "json"
|
121
|
+
}
|
122
|
+
```
|
123
|
+
|
124
|
+
Although one would obviously want to use a more dependency-injection-esque style when choosing `Adalog::SimpleLoggingAdapter` over whatever the production-mode default adapter is for the service in question.
|
125
|
+
|
126
|
+
## Project Status and Goals
|
127
|
+
|
128
|
+
This project adheres to [Semantic Versioning](http://semver.org/), including the part whereby being pre-1.0 means that all bets are off, code can and will change radically, and that it should not be considered "production-ready" until said time as version `1.0.0` is declared.
|
129
|
+
|
130
|
+
This library is being test-driven by its inclusion within a functioning production application, so rest assured it will get there soon enough. In the mean time, feedback is welcome. Bug reports about functionality that the README claims works but doesn't is welcome. But so as not to distract from the original goals, contributions in the form of pull requests will languish until the project's initial functionality is completed.
|
131
|
+
|
132
|
+
|
133
|
+
### Remaining Work Prior to 1.0.0
|
134
|
+
|
135
|
+
- Customization of ERB Layout.
|
136
|
+
- Customization of Sinatra Layout.
|
137
|
+
- Time formats that allow for strictly numeric time or even string-based logical time.
|
138
|
+
- A quick script to color-code entries with the same title.
|
139
|
+
- Implementations for `InMemoryRepo#fetch` and `PStoreRepo#fetch`.
|
140
|
+
- Tests for the three built-in repository classes.
|
141
|
+
- A `StubLoggingAdapter` which can be pre-programmed with responses to certain messages.
|
142
|
+
- A `MockLoggingAdapter` which can be pre-programmed with responses to certain messages and asked to enforce that some messages were received.
|
143
|
+
- A method (working name `MultiWeb`) to allow multiple Sinatra apps to be generated, in case one wants to have a separate route-and-repo combination for every adapter.
|
144
|
+
|
data/Rakefile
ADDED
data/adalog.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'adalog/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "adalog"
|
8
|
+
spec.version = Adalog::VERSION
|
9
|
+
spec.platform = Gem::Platform::RUBY
|
10
|
+
spec.authors = ["Paul Kwiatkowski"]
|
11
|
+
spec.email = ["paul@groupraise.com"]
|
12
|
+
spec.summary = %q{A not-quite logger, with several storage methods. Records occurrences, provides a web app for viewing entries.}
|
13
|
+
spec.description = %q{A not-quite logger, with several storage methods. Records occurrences, provides a web app for viewing entries. Motivated by the need to record the actions of stubbed-out versions of adapters to third-party services, and for that record to be something other than a traditional logger. Web viewer is built in Sinatra and so can be run standalone via the usual methods or mounted at some route within a Rails app. Comes with three repository implementations for storing/retrieving entries and is trivial to write your own.}
|
14
|
+
spec.homepage = "https://github.com/swifthand/adalog"
|
15
|
+
spec.license = "Revised BSD, see LICENSE.md"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "sinatra", "~> 1.4", ">= 1.4.4"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.1"
|
26
|
+
spec.add_development_dependency "turn-again-reporter", "~> 1.1", ">= 1.1.0"
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Adalog
|
2
|
+
class ActiveRecordRepo
|
3
|
+
|
4
|
+
attr_reader :record_class, :base_relation
|
5
|
+
|
6
|
+
def initialize(record_class, **repo_options)
|
7
|
+
@record_class = record_class
|
8
|
+
@base_relation = determine_base_relation(repo_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def fetch(**options)
|
13
|
+
where_options = options.fetch(:where, {})
|
14
|
+
order_options = options.fetch(:order, :none)
|
15
|
+
relation = relation_from_options(where_options, order_options)
|
16
|
+
if options[:first]
|
17
|
+
relation.first(options[:first])
|
18
|
+
elsif options[:last]
|
19
|
+
relation.last(options[:last])
|
20
|
+
else
|
21
|
+
relation.to_a
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def insert(attr_hash = {}, **attr_args)
|
27
|
+
attrs = attr_hash.merge(attr_args)
|
28
|
+
record = record_class.new(**attrs)
|
29
|
+
if record.valid?
|
30
|
+
if record.save
|
31
|
+
[:ok, record]
|
32
|
+
else
|
33
|
+
wtf = "Unknown Non-validation error in call to #{record_class}#save"
|
34
|
+
[:error, [wtf]]
|
35
|
+
end
|
36
|
+
else
|
37
|
+
[:error, record.errors.full_messages]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def all
|
43
|
+
record_class.all.to_a
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private ########################################################################
|
48
|
+
|
49
|
+
|
50
|
+
def relation_from_options(where, order)
|
51
|
+
relation = base_relation.dup
|
52
|
+
relation = relation.where(where) if where.any?
|
53
|
+
relation = relation.order(order) if order != :none
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def determine_base_relation(options)
|
58
|
+
options.fetch(:base_relation, record_class.unscoped)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Adalog
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
RequiredSettings = [:repo, :singleton, :html_erb, :time_format, :web_heading].freeze
|
6
|
+
UntouchedValue = Object.new.freeze
|
7
|
+
|
8
|
+
attr_accessor *RequiredSettings
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
defaults.each_pair do |attr, value|
|
12
|
+
self.send("#{attr}=", value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def defaults
|
18
|
+
{ repo: Adalog::InMemoryRepo.new,
|
19
|
+
singleton: true,
|
20
|
+
html_erb: true,
|
21
|
+
time_format: "%H:%M:%S - %d %b %Y",
|
22
|
+
web_heading: "Stub Adapter Logs",
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def web_defaults
|
28
|
+
{ repo: self.repo,
|
29
|
+
time_format: self.time_format,
|
30
|
+
heading: self.web_heading,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def validate!
|
36
|
+
RequiredSettings.each do |required_attr|
|
37
|
+
if UntouchedValue == self.send(required_attr)
|
38
|
+
raise "Setting '#{required_attr}' for Adalog left unconfigured."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/adalog/entry.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Adalog
|
5
|
+
class Entry
|
6
|
+
|
7
|
+
def self.build(obj = nil, **options)
|
8
|
+
arguments =
|
9
|
+
if nil == obj
|
10
|
+
options
|
11
|
+
elsif obj.is_a?(Hash) || obj.respond_to?(:[])
|
12
|
+
obj
|
13
|
+
elsif obj.respond_to?(:to_h)
|
14
|
+
obj.to_h.merge(options)
|
15
|
+
else
|
16
|
+
{ title: obj.respond_to?(:title) && obj.title,
|
17
|
+
timestamp: obj.respond_to?(:timestamp) && obj.timestamp,
|
18
|
+
message: obj.respond_to?(:message) && obj.message,
|
19
|
+
details: obj.respond_to?(:details) && obj.details,
|
20
|
+
format: obj.respond_to?(:format) && obj.format,
|
21
|
+
}.merge(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
self.new(
|
25
|
+
title: arguments[:title] || arguments['title'],
|
26
|
+
timestamp: arguments[:timestamp] || arguments['timestamp'],
|
27
|
+
message: arguments[:message] || arguments['message'],
|
28
|
+
details: arguments[:details] || arguments['details'],
|
29
|
+
format: arguments[:format] || arguments['format'],
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :title, :timestamp, :message, :details, :errors
|
34
|
+
|
35
|
+
def initialize(title: nil, timestamp: nil, message: nil, details: nil, format: nil)
|
36
|
+
@title = title || ''
|
37
|
+
@timestamp = timestamp || Time.now
|
38
|
+
@message = message || ''
|
39
|
+
@details = details || ''
|
40
|
+
@format = format || 'json'
|
41
|
+
validate!
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def valid?
|
46
|
+
@errors.none?
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# TODO: Make this something we store and/or can override.
|
51
|
+
def format
|
52
|
+
:json
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def details_blank?
|
57
|
+
blank?(details)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
private ######################################################################
|
62
|
+
|
63
|
+
|
64
|
+
def validate!
|
65
|
+
@errors = []
|
66
|
+
errors << content_error if no_content?
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def content_error
|
71
|
+
"Must have at least one of: 'title', 'message', 'details'."
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def no_content?
|
76
|
+
blank?(title) && blank?(message) && blank?(details)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def blank?(val)
|
81
|
+
nil == val || '' == val
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Adalog
|
2
|
+
class InMemoryRepo
|
3
|
+
|
4
|
+
attr_reader :storage
|
5
|
+
|
6
|
+
def initialize(**repo_options)
|
7
|
+
@storage = Array.new
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def fetch(**options)
|
12
|
+
all
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def insert(entry = nil, **options)
|
17
|
+
converted_entry = Adalog::Entry.build(entry = nil, **options)
|
18
|
+
if converted_entry.valid?
|
19
|
+
storage.unshift(converted_entry)
|
20
|
+
[:ok, converted_entry]
|
21
|
+
else
|
22
|
+
[:error, converted_entry.errors]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def clear!
|
28
|
+
@storage = Array.new
|
29
|
+
:ok
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def all
|
34
|
+
storage.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module Adalog
|
4
|
+
class PStoreRepo
|
5
|
+
|
6
|
+
attr_reader :storage
|
7
|
+
|
8
|
+
def initialize(path, **repo_options)
|
9
|
+
@storage = PStore.new(path, true)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def fetch(**options)
|
14
|
+
all
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def all
|
19
|
+
storage.transaction do
|
20
|
+
storage.roots.flat_map do |key|
|
21
|
+
storage.fetch(key, [])
|
22
|
+
end
|
23
|
+
end.reverse
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def insert(entry = nil, **options)
|
28
|
+
converted_entry = Adalog::Entry.build(entry, **options)
|
29
|
+
if converted_entry.valid?
|
30
|
+
insert_into_storage(converted_entry)
|
31
|
+
[:ok, converted_entry]
|
32
|
+
else
|
33
|
+
[:error, converted_entry.errors]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def clear!
|
39
|
+
storage.transaction do
|
40
|
+
all_keys = storage.roots
|
41
|
+
all_keys.each do |key|
|
42
|
+
storage.delete(key)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
:ok
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private ######################################################################
|
50
|
+
|
51
|
+
|
52
|
+
def insert_into_storage(entry)
|
53
|
+
storage.transaction do
|
54
|
+
key = Time.now.to_i
|
55
|
+
list = storage.fetch(key, [])
|
56
|
+
list.unshift(entry)
|
57
|
+
storage[key] = list
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Adalog
|
2
|
+
class SimpleLoggingAdapter
|
3
|
+
|
4
|
+
attr_reader :service_name, :repo
|
5
|
+
|
6
|
+
def initialize(service_name, repo)
|
7
|
+
@service_name = service_name
|
8
|
+
@repo = repo
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# TODO: Record something w.r.t. whether or not a block is given?
|
13
|
+
def method_missing(msg, *args, &block)
|
14
|
+
repo.insert(
|
15
|
+
title: service_name,
|
16
|
+
message: msg,
|
17
|
+
details: args)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
adalog.entriesList = (function(js) {
|
2
|
+
|
3
|
+
var showButtonClass = '-show-details';
|
4
|
+
var hideButtonClass = '-hide-details';
|
5
|
+
var detailsContentClass = '-details-content';
|
6
|
+
var toggleDetailsClass = '-toggle-details';
|
7
|
+
|
8
|
+
|
9
|
+
var init = function init() {
|
10
|
+
js.docReady(function() {
|
11
|
+
formatDetailsJSON();
|
12
|
+
// initVisibilityButtons();
|
13
|
+
initDetailsToggle();
|
14
|
+
});
|
15
|
+
};
|
16
|
+
|
17
|
+
|
18
|
+
var formatDetailsJSON = function formatDetailsJSON() {
|
19
|
+
var detailsElements = document.getElementsByClassName('-format-as-json');
|
20
|
+
js.convertArrayLike(detailsElements).forEach(function(elt) {
|
21
|
+
try {
|
22
|
+
var contentsAsObj = JSON.parse(elt.textContent);
|
23
|
+
elt.textContent = JSON.stringify(contentsAsObj, null, 2);
|
24
|
+
} catch(exc) {
|
25
|
+
console.log("Failed to parse details of element that was expicitly flagged with '-format-as-json'.");
|
26
|
+
}
|
27
|
+
});
|
28
|
+
};
|
29
|
+
|
30
|
+
|
31
|
+
var initDetailsToggle = function initDetailsToggle() {
|
32
|
+
var detailsToggles = document.getElementsByClassName(toggleDetailsClass);
|
33
|
+
var detailsRegions = document.getElementsByClassName(detailsContentClass);
|
34
|
+
|
35
|
+
js.convertArrayLike(detailsToggles).forEach(function(elt) {
|
36
|
+
elt.addEventListener('click', function(evt) {
|
37
|
+
var detailsContent = evt.currentTarget.getElementsByClassName(detailsContentClass)[0];
|
38
|
+
js.eltDisplayToggle(detailsContent, 'block');
|
39
|
+
})
|
40
|
+
});
|
41
|
+
|
42
|
+
js.convertArrayLike(detailsRegions).forEach(js.eltHide);
|
43
|
+
};
|
44
|
+
|
45
|
+
|
46
|
+
var initVisibilityButtons = function initVisibilityButtons() {
|
47
|
+
var showButtons = document.getElementsByClassName(showClass);
|
48
|
+
var hideButtons = document.getElementsByClassName(hideClass);
|
49
|
+
var detailsRegions = document.getElementsByClassName(detailsContentClass);
|
50
|
+
var initiallyHidden = js.convertArrayLike(detailsRegions).concat(js.convertArrayLike(hideButtons));
|
51
|
+
|
52
|
+
// Click events for show buttons show content, show a hide button and hide themselves.
|
53
|
+
js.convertArrayLike(showButtons).forEach(function(elt) {
|
54
|
+
elt.addEventListener('click', function(evt) {
|
55
|
+
var hideSibling = js.findSiblingByClassName(evt.currentTarget, hideClass);
|
56
|
+
var details = js.findSiblingByClassName(evt.currentTarget, detailsContentClass);
|
57
|
+
js.eltShowBlock(details);
|
58
|
+
js.eltShowILBlock(hideSibling);
|
59
|
+
js.eltHide(evt.currentTarget);
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
// Click events for hide buttons hide content, show a show button and hide themselves.
|
64
|
+
js.convertArrayLike(hideButtons).forEach(function(elt) {
|
65
|
+
elt.addEventListener('click', function(evt) {
|
66
|
+
var showSibling = js.findSiblingByClassName(evt.currentTarget, showClass);
|
67
|
+
var details = js.findSiblingByClassName(evt.currentTarget, detailsContentClass);
|
68
|
+
js.eltHide(details);
|
69
|
+
js.eltShowBlock(showSibling);
|
70
|
+
js.eltHide(evt.currentTarget);
|
71
|
+
});
|
72
|
+
});
|
73
|
+
|
74
|
+
initiallyHidden.forEach(js.eltHide);
|
75
|
+
};
|
76
|
+
|
77
|
+
|
78
|
+
return {
|
79
|
+
'init': init
|
80
|
+
};
|
81
|
+
|
82
|
+
})(adalog.vanillaJS);
|