majoun 0.0.1

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.
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 50.5
data/lib/cookie.rb ADDED
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ require 'time'
4
+ require 'base64'
5
+
6
+ require 'concord'
7
+ require 'adamantium'
8
+ require 'abstract_type'
9
+
10
+ # Models an HTTP Cookie
11
+ class Cookie
12
+
13
+ # An empty frozen array
14
+ EMPTY_ARRAY = [].freeze
15
+
16
+ # An empty frozen hash
17
+ EMPTY_HASH = {}.freeze
18
+
19
+ # An empty frozen string
20
+ EMPTY_STRING = ''.freeze
21
+
22
+ # Separates the cookie name from its value
23
+ NAME_VALUE_SEPARATOR = '='.freeze
24
+
25
+ # Separates cookies
26
+ COOKIE_SEPARATOR = '; '.freeze
27
+
28
+ # Separates ruby class names in a FQN
29
+ DOUBLE_COLON = '::'.freeze
30
+
31
+ # Helper for cookie deletion
32
+ class Empty < self
33
+ def initialize(name)
34
+ super(name, nil)
35
+ end
36
+ end
37
+
38
+ # Namespace for cookie encoders
39
+ module Encoder
40
+ Base64 = ->(string) { ::Base64.urlsafe_encode64(string) }
41
+ end
42
+
43
+ # Namespace for cookie decoders
44
+ module Decoder
45
+ Base64 = ->(string) { ::Base64.urlsafe_decode64(string) }
46
+ end
47
+
48
+ # Cookie error base class
49
+ Error = Class.new(StandardError)
50
+
51
+ include Concord::Public.new(:name, :value)
52
+ include Adamantium::Flat
53
+
54
+ def self.coerce(string)
55
+ new(*string.split(NAME_VALUE_SEPARATOR, 2))
56
+ end
57
+
58
+ def encode(encoder = Encoder::Base64)
59
+ new(encoder.call(value))
60
+ end
61
+
62
+ def decode(decoder = Decoder::Base64)
63
+ new(decoder.call(value))
64
+ end
65
+
66
+ def encrypt(box)
67
+ new(box.encrypt(value))
68
+ end
69
+
70
+ def decrypt(box)
71
+ new(box.decrypt(value))
72
+ end
73
+
74
+ def to_s
75
+ "#{name}=#{value}"
76
+ end
77
+ memoize :to_s
78
+
79
+ private
80
+
81
+ def new(new_value)
82
+ self.class.new(name, new_value)
83
+ end
84
+
85
+ end # class Cookie
86
+
87
+ require 'cookie/header'
88
+ require 'cookie/header/attribute'
89
+ require 'cookie/registry'
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Models a transient, new cookie on the server that can be serialized
6
+ # into an HTTP 'Set-Cookie' header
7
+ class Header
8
+
9
+ include Equalizer.new(:cookie, :attributes)
10
+ include Adamantium::Flat
11
+
12
+ attr_reader :cookie
13
+ protected :cookie
14
+
15
+ attr_reader :attributes
16
+ protected :attributes
17
+
18
+ def self.build(name, value, attributes)
19
+ new(Cookie.new(name, value), attributes)
20
+ end
21
+
22
+ def initialize(cookie, attributes = Attribute::Set::EMPTY)
23
+ @cookie, @attributes = cookie, attributes
24
+ end
25
+
26
+ def with_domain(domain)
27
+ with_attribute(Attribute::Domain.new(domain))
28
+ end
29
+
30
+ def with_path(path)
31
+ with_attribute(Attribute::Path.new(path))
32
+ end
33
+
34
+ def with_max_age(seconds)
35
+ with_attribute(Attribute::MaxAge.new(seconds))
36
+ end
37
+
38
+ def with_expires(time)
39
+ with_attribute(Attribute::Expires.new(time))
40
+ end
41
+
42
+ def secure
43
+ with_attribute(Attribute::Secure.instance)
44
+ end
45
+
46
+ def http_only
47
+ with_attribute(Attribute::HttpOnly.instance)
48
+ end
49
+
50
+ def delete
51
+ new(Empty.new(cookie.name), attributes.merge(Attribute::Expired))
52
+ end
53
+
54
+ def to_s
55
+ "#{cookie}#{attributes}"
56
+ end
57
+ memoize :to_s
58
+
59
+ private
60
+
61
+ def with_attribute(attribute)
62
+ new(cookie, attributes.merge(attribute))
63
+ end
64
+
65
+ def new(cookie, attributes)
66
+ self.class.new(cookie, attributes)
67
+ end
68
+
69
+ end # class Header
70
+ end # class Cookie
@@ -0,0 +1,172 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+ class Header
5
+
6
+ # Baseclass for cookie attributes
7
+ class Attribute
8
+
9
+ include Concord::Public.new(:name)
10
+ include Adamantium
11
+
12
+ REGISTRY = {}
13
+
14
+ def self.coerce(name, value)
15
+ REGISTRY.fetch(name.to_sym).build(value)
16
+ end
17
+
18
+ def self.register_as(name)
19
+ REGISTRY[name.to_sym] = self
20
+ end
21
+
22
+ def self.attribute_name
23
+ name.split(DOUBLE_COLON).last
24
+ end
25
+
26
+ def to_s
27
+ name
28
+ end
29
+
30
+ # Models a set of attributes used within a {Serializable} cookie
31
+ class Set
32
+
33
+ include Concord.new(:attributes)
34
+ include Enumerable
35
+ include Adamantium
36
+
37
+ def self.coerce(attributes)
38
+ new(attributes.each_with_object({}) { |(name, value), hash|
39
+ attribute = Attribute.coerce(name, value)
40
+ hash[attribute.name] = attribute if attribute
41
+ })
42
+ end
43
+
44
+ def each(&block)
45
+ return to_enum unless block
46
+ attributes.each_value(&block)
47
+ self
48
+ end
49
+
50
+ def merge(attribute)
51
+ Set.new(attributes.merge(attribute.name => attribute))
52
+ end
53
+
54
+ def to_s
55
+ "#{COOKIE_SEPARATOR}#{map(&:to_s).join(COOKIE_SEPARATOR)}"
56
+ end
57
+ memoize :to_s
58
+
59
+ # An empty {Set} to be serialized to {EMPTY_STRING}
60
+ class Empty < self
61
+
62
+ def initialize
63
+ super(EMPTY_HASH)
64
+ end
65
+
66
+ def to_s
67
+ EMPTY_STRING
68
+ end
69
+
70
+ end # class Empty
71
+
72
+ EMPTY = Empty.new
73
+
74
+ end # class Set
75
+
76
+ # Abstract baseclass for attributes that have no value
77
+ #
78
+ # @abstract
79
+ class Unary < self
80
+
81
+ include AbstractType
82
+
83
+ CACHE = {}
84
+
85
+ def self.build(value)
86
+ value ? instance : nil
87
+ end
88
+
89
+ def self.instance
90
+ CACHE.fetch(attribute_name) {
91
+ CACHE[attribute_name] = new(attribute_name)
92
+ }
93
+ end
94
+
95
+ end
96
+
97
+ # Abstract baseclass for attributes that consist of a name-value
98
+ # pair
99
+ #
100
+ # @abstract
101
+ class Binary < self
102
+
103
+ include AbstractType
104
+ include Equalizer.new(:name, :value)
105
+
106
+ attr_reader :value
107
+ protected :value
108
+
109
+ def self.build(value)
110
+ new(value)
111
+ end
112
+
113
+ def initialize(value)
114
+ super(self.class.attribute_name)
115
+ @value = value
116
+ end
117
+
118
+ def to_s
119
+ "#{name}=#{serialized_value}"
120
+ end
121
+ memoize :to_s
122
+
123
+ private
124
+
125
+ def serialized_value
126
+ value
127
+ end
128
+
129
+ end # class Binary
130
+
131
+ # The Domain attribute
132
+ class Domain < Binary
133
+ register_as :domain
134
+ end
135
+
136
+ # The Path attribute
137
+ class Path < Binary
138
+ register_as :path
139
+ end
140
+
141
+ # The Max-Age attribute
142
+ class MaxAge < Binary
143
+ register_as :max_age
144
+ end
145
+
146
+ # The Expires attribute
147
+ class Expires < Binary
148
+ register_as :expires
149
+
150
+ private
151
+
152
+ def serialized_value
153
+ super.dup.gmtime.rfc2822
154
+ end
155
+ end
156
+
157
+ # The Secure attribute
158
+ class Secure < Unary
159
+ register_as :secure
160
+ end
161
+
162
+ # The HttpOnly attribute
163
+ class HttpOnly < Unary
164
+ register_as :http_only
165
+ end
166
+
167
+ # Already expired {Expires} attribute useful for cookie deletion
168
+ Expired = Expires.new(Time.at(0))
169
+
170
+ end # class Attribute
171
+ end # class Header
172
+ end # class Cookie
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Models a registry of {Cookie} instances
6
+ class Registry
7
+
8
+ include Enumerable
9
+ include Equalizer.new(:entries)
10
+ include Adamantium::Flat
11
+
12
+ # Message for {UnknownCookieError}
13
+ UNKNOWN_COOKIE_MSG = 'No cookie named %s is registered'.freeze
14
+
15
+ # Raised when trying to {#fetch} an unknown {Cookie}
16
+ UnknownCookieError = Class.new(Cookie::Error)
17
+
18
+ def self.coerce(header)
19
+ new(cookie_hash(header))
20
+ end
21
+
22
+ def self.cookie_hash(header)
23
+ header.split(COOKIE_SEPARATOR).each_with_object({}) { |string, hash|
24
+ cookie = Cookie.coerce(string)
25
+ hash[cookie.name] = cookie
26
+ }
27
+ end
28
+
29
+ private_class_method :cookie_hash
30
+
31
+ attr_reader :entries
32
+ protected :entries
33
+
34
+ def initialize(entries = EMPTY_HASH)
35
+ @entries = entries
36
+ end
37
+
38
+ def each(&block)
39
+ return to_enum unless block
40
+ entries.each(&block)
41
+ self
42
+ end
43
+
44
+ def fetch(name)
45
+ get(name) or raise UnknownCookieError, UNKNOWN_COOKIE_MSG % name.inspect
46
+ end
47
+
48
+ def get(name)
49
+ @entries[name]
50
+ end
51
+
52
+ end # class Registry
53
+ end # class Cookie
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Gem version
6
+ VERSION = '0.0.1'.freeze
7
+
8
+ end
data/majoun.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/cookie/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "majoun"
7
+ gem.version = Cookie::VERSION.dup
8
+ gem.authors = [ "Martin Gamsjaeger (snusnu)" ]
9
+ gem.email = [ "gamsnjaga@gmail.com" ]
10
+ gem.description = "An HTTP cookie implemented in ruby"
11
+ gem.summary = "Support for sending and receiving HTTP cookies on ruby servers"
12
+ gem.homepage = "https://github.com/snusnu/cookie"
13
+
14
+ gem.require_paths = [ "lib" ]
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
17
+ gem.extra_rdoc_files = %w[LICENSE README.md TODO.md]
18
+ gem.license = 'MIT'
19
+
20
+ gem.add_dependency 'adamantium', '~> 0.2.0'
21
+ gem.add_dependency 'abstract_type', '~> 0.0.7'
22
+ gem.add_dependency 'concord', '~> 0.1.4'
23
+
24
+ gem.add_development_dependency 'bundler', '~> 1.6.1'
25
+ end
@@ -0,0 +1,186 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Spec
6
+ class CryptoBox
7
+ def encrypt(string)
8
+ string
9
+ end
10
+
11
+ def decrypt(string)
12
+ string
13
+ end
14
+ end
15
+
16
+ module Encoder
17
+ Noop = ->(string) { string }
18
+ end
19
+
20
+ module Decoder
21
+ Noop = ->(string) { string }
22
+ end
23
+ end
24
+
25
+ shared_context 'integration specs' do
26
+ let(:cookie) { Cookie.new(name, value) }
27
+ let(:name) { 'SID' }
28
+ let(:value) { '{"id": 11}' } # triggers padding char '=' when base64 encoded
29
+ end
30
+
31
+ describe Cookie do
32
+
33
+ include_context 'integration specs'
34
+
35
+ let(:encoder) { Spec::Encoder::Noop }
36
+ let(:decoder) { Spec::Decoder::Noop }
37
+ let(:box) { Spec::CryptoBox.new }
38
+
39
+ it 'supports coercion from string to a Cookie instance' do
40
+ c = cookie
41
+ expect(Cookie.coerce(c.to_s)).to eql(cookie)
42
+
43
+ c = cookie.encode
44
+ expect(Cookie.coerce(c.to_s)).to eql(cookie.encode)
45
+
46
+ c = c.decode
47
+ expect(Cookie.coerce(c.to_s)).to eql(cookie)
48
+ end
49
+
50
+ it 'supports #name and #value' do
51
+ expect(cookie.name).to be(name)
52
+ expect(cookie.value).to be(value)
53
+ end
54
+
55
+ it 'supports #to_s' do
56
+ expect(cookie.to_s).to eql("#{cookie.name}=#{cookie.value}")
57
+ end
58
+
59
+ context 'encoding and decoding' do
60
+ it 'defaults to base64' do
61
+ encoded = cookie.encode
62
+ expect(encoded).to eql(Cookie.new(name, Base64.urlsafe_encode64(cookie.value)))
63
+
64
+ decoded = encoded.decode
65
+ expect(decoded).to eql(Cookie.new(name, Base64.urlsafe_decode64(encoded.value)))
66
+ end
67
+
68
+ it 'supports custom encoders and decoders' do
69
+ encoded = cookie.encode(encoder)
70
+ expect(encoded).to eql(Cookie.new(name, encoder.call(cookie.value)))
71
+
72
+ decoded = encoded.decode(decoder)
73
+ expect(decoded).to eql(Cookie.new(name, decoder.call(encoded.value)))
74
+ end
75
+ end
76
+
77
+ context 'encryption and decryption' do
78
+ it 'supports crypto boxes which provide #encrypt(msg) and #decrypt(msg)' do
79
+ encrypted = cookie.encrypt(box)
80
+ expect(encrypted).to eql(Cookie.new(name, box.encrypt(cookie.value)))
81
+
82
+ decrypted = encrypted.decrypt(box)
83
+ expect(decrypted).to eql(Cookie.new(name, box.decrypt(encrypted.value)))
84
+ end
85
+ end
86
+ end
87
+
88
+ describe Cookie::Header do
89
+
90
+ include_context 'integration specs'
91
+
92
+ let(:definition) { Cookie::Header.new(cookie) }
93
+
94
+ it 'supports all cookie attributes' do
95
+ d = definition
96
+ expect(d.to_s).to eql('SID={"id": 11}')
97
+
98
+ d = definition.with_domain('.foo.bar')
99
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar')
100
+
101
+ d = d.with_path('/foo')
102
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo')
103
+
104
+ d = d.with_expires(Time.at(42))
105
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000')
106
+
107
+ d = d.with_max_age(42)
108
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42')
109
+
110
+ d = d.secure
111
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure')
112
+
113
+ d = d.http_only
114
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure; HttpOnly')
115
+
116
+ d = definition.secure
117
+ expect(d.to_s).to eql('SID={"id": 11}; Secure')
118
+
119
+ d = d.http_only
120
+ expect(d.to_s).to eql('SID={"id": 11}; Secure; HttpOnly')
121
+
122
+ d = definition.http_only
123
+ expect(d.to_s).to eql('SID={"id": 11}; HttpOnly')
124
+
125
+ d = d.secure
126
+ expect(d.to_s).to eql('SID={"id": 11}; HttpOnly; Secure')
127
+ end
128
+
129
+ it 'overwrites previously defined attributes' do
130
+ d = definition.with_max_age(0).with_max_age(42)
131
+ expect(d.to_s).to eql('SID={"id": 11}; MaxAge=42')
132
+ end
133
+
134
+ it 'supports deleting cookies on the client' do
135
+ d = definition.delete
136
+ expect(d.to_s).to eql('SID=; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
137
+
138
+ d = definition.with_domain('.foo.bar').with_path('/foo')
139
+ d = d.delete
140
+ expect(d.to_s).to eql('SID=; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
141
+ end
142
+ end
143
+
144
+ describe Cookie::Registry do
145
+
146
+ include_context 'integration specs'
147
+
148
+ let(:registry) { Cookie::Registry.coerce(header) }
149
+ let(:header) { cookie.to_s }
150
+
151
+ it 'contains no entries after #initialize' do
152
+ expect(Cookie::Registry.new.count).to be(0)
153
+ end
154
+
155
+ it 'supports coercion from a Set-Cookie header' do
156
+ expect(registry.get(name)).to eql(cookie)
157
+ end
158
+
159
+ it 'supports #get(name)' do
160
+ expect(registry.get(name)).to eql(cookie)
161
+ expect(registry.get(:foo)).to be(nil)
162
+ end
163
+
164
+ it 'supports #fetch(name)' do
165
+ expect(registry.fetch(name)).to eql(cookie)
166
+ expect { registry.fetch(:foo) }.to raise_error(Cookie::Registry::UnknownCookieError)
167
+ end
168
+
169
+ it 'is an Enumerable' do
170
+ expect(registry).to be_kind_of(Enumerable)
171
+ end
172
+
173
+ context '#each' do
174
+ it 'returns self when a block is given' do
175
+ expect(registry.each { |_| }).to be(registry)
176
+ end
177
+
178
+ it 'returns an enumerator when no block is given' do
179
+ expect(registry.each).to be_instance_of(Enumerator)
180
+ end
181
+
182
+ it 'yields all cookies' do
183
+ expect { |block| registry.each(&block) }.to yield_successive_args([name, cookie])
184
+ end
185
+ end
186
+ end