gestalt
Advanced tools
Comparing version 0.0.7 to 0.0.8
@@ -14,3 +14,3 @@ var gestalt = require('../lib/gestalt'); | ||
}, | ||
original: new gestalt.ConfigEnv() | ||
config: new gestalt.ConfigEnv() | ||
}); | ||
@@ -25,3 +25,3 @@ | ||
}, | ||
original: new gestalt.ConfigArgs( { | ||
config: new gestalt.ConfigArgs( { | ||
optimist_usage: "Usage: $0 options\nStart a test cluster talking to zookeeper to coordinate naming and leader election.", | ||
@@ -112,3 +112,2 @@ optimist_options: { | ||
//console.log( require.resolve( config.get('config:file'))); | ||
var file = new gestalt.ConfigFile( { source: require.resolve( config.get( 'config:file' ) ), | ||
@@ -115,0 +114,0 @@ watch: true, |
@@ -29,4 +29,4 @@ var gestalt = require('../lib/gestalt'); | ||
var f = new ConfigFile({ source: yaml_file, format: 'yaml'}); | ||
var re = new RemapConfig( { mapper: env_mapper, original: e } ); | ||
var ra = new RemapConfig( { mapper: args_mapper, original: a } ); | ||
var re = new RemapConfig( { mapper: env_mapper, config: e } ); | ||
var ra = new RemapConfig( { mapper: args_mapper, config: a } ); | ||
@@ -33,0 +33,0 @@ var config = new ConfigContainer({ source: "config" }); |
@@ -23,3 +23,3 @@ var gestalt = require('../lib/gestalt'), | ||
var r = new RemapConfig( { mapper: mapper, original: c } ); | ||
var r = new RemapConfig( { mapper: mapper, config: c } ); | ||
@@ -26,0 +26,0 @@ console.log( r.get('new:foo:0') ); |
@@ -26,7 +26,6 @@ var EventEmitter = require('events').EventEmitter, | ||
this._pattern_listeners_ = []; | ||
this._state_ = 'not ready'; | ||
var self = this; | ||
this.on('change', function(change) { this._on_change_(change) }); | ||
process.nextTick( function() {self.state( self._options_.initial_state_ ) } ); | ||
self.state( self._options_.initial_state ); | ||
} | ||
@@ -38,2 +37,19 @@ | ||
function worse_state( a , b ) { | ||
if( a == 'invalid' || b == 'invalid' ) { | ||
return 'invalid'; | ||
} else if ( a == 'ready' ) { | ||
return b; | ||
} else { | ||
return a | ||
} | ||
} | ||
p._dependency_states_ = function() { | ||
return _.chain( this._values_ ) | ||
.filter( function(value) { return value instanceof Configuration } ) | ||
.map( function(value) {return value.state() } ) | ||
.value(); | ||
} | ||
p.state = function(state, data) { | ||
@@ -44,4 +60,13 @@ if( ! state ) { | ||
var worst = _.reduce( this._dependency_states_(), worse_state, state ); | ||
if( state != worst ) { | ||
// unable to change states because there | ||
// is a dependency blocking us with a | ||
// higher priority state. | ||
state = worst; | ||
} | ||
if( this._state_ != state || state == 'invalid' ) { | ||
var so = { state: state, old_state: this._state, data: data }; | ||
var so = { state: state, old_state: this._state_, data: data }; | ||
this._state_ = state; | ||
@@ -86,3 +111,3 @@ this.emit('state', so); | ||
p.getPath = function(name) { | ||
p._get_path_ = function(name) { | ||
var path; | ||
@@ -107,2 +132,3 @@ if( typeof(name)== 'string' ) { | ||
}); | ||
self.emit('state', { state: this._state_, old_state: this._state_ } ); | ||
}; | ||
@@ -141,3 +167,3 @@ | ||
p.get = function(name) { | ||
var path = this.getPath(name); | ||
var path = this._get_path_(name); | ||
if( _.isUndefined(path)) { | ||
@@ -160,3 +186,3 @@ return undefined; | ||
p.getValSource = function(name) { | ||
var path = this.getPath(name); | ||
var path = this._get_path_(name); | ||
if( _.isUndefined(path)) { | ||
@@ -183,3 +209,3 @@ return undefined; | ||
var self = this; | ||
var path = this.getPath(name); | ||
var path = this._get_path_(name); | ||
if( _.isUndefined(path)) { | ||
@@ -240,5 +266,5 @@ return; | ||
if( state_change.state == 'invalid' ) { | ||
this.state( 'invalid', state_change.data ); | ||
self.state( 'invalid', state_change.data ); | ||
} else if (state_change.state == 'ready' ) { | ||
if( _.chain( this._values_) | ||
if( _.chain( self._values_) | ||
.values() | ||
@@ -249,6 +275,6 @@ .filter( function(x) { return x instanceof Configuration ;} ) | ||
) { | ||
this.state('ready', state_change.data ); | ||
self.state('ready', state_change.data ); | ||
} | ||
} else { | ||
this.state( 'not ready', data ); | ||
self.state( 'not ready', state_change.data ); | ||
} | ||
@@ -301,3 +327,3 @@ } | ||
var path = this.getPath(name); | ||
var path = this._get_path_(name); | ||
@@ -324,3 +350,3 @@ // do nothing if the path is not valid | ||
// remove values that are not present in the new value | ||
_.each( this._values_[n].flat_keys, function(key) { | ||
_.each( this._values_[n]._child_keys_(), function(key) { | ||
if( !_.has(value, key) ) { | ||
@@ -358,3 +384,3 @@ self.remove([n,key]); | ||
} else { | ||
this._change_value_( n, new Configuration( this._options_ ) ); | ||
this._change_value_( n, new Configuration( _.extend({},this._options_,{initial_state: 'ready'} ) ) ); | ||
this._sources_[n] = source; | ||
@@ -368,3 +394,3 @@ this._values_[n].set(path,value,source); | ||
var self = this; | ||
var path = this.getPath(name); | ||
var path = this._get_path_(name); | ||
var n = path.shift(); | ||
@@ -394,3 +420,5 @@ if( path.length === 0 ) { | ||
p.flat_keys = function() { | ||
// Return the keys of direct descendents of this config | ||
// object. | ||
p._child_keys_ = function() { | ||
var keys = []; | ||
@@ -408,11 +436,11 @@ return _.keys( this._values_ ); | ||
p.isArrayLike = function() { | ||
var arrayLike = true; | ||
this.flat_keys().map( function(x) { return(Number(x)); }) | ||
.sort() | ||
.forEach( function(element,index) { | ||
arrayLike = arrayLike && (element===index); | ||
}); | ||
return arrayLike; | ||
}; | ||
// p.isArrayLike = function() { | ||
// var arrayLike = true; | ||
// this.flat_keys().map( function(x) { return(Number(x)); }) | ||
// .sort() | ||
// .forEach( function(element,index) { | ||
// arrayLike = arrayLike && (element===index); | ||
// }); | ||
// return arrayLike; | ||
// }; | ||
@@ -447,3 +475,3 @@ //p.toObject = function() { | ||
_.each( this.keys().sort(), function(key) { | ||
var path = self.getPath(key); | ||
var path = self._get_path_(key); | ||
var indent = ""; | ||
@@ -503,3 +531,3 @@ for(var i=0; i<keys.length && i <path.length && keys[i] == path[i]; ++i) { | ||
_.each( this.keys().sort() , function(key) { | ||
var path = self.getPath(key); | ||
var path = self._get_path_(key); | ||
var val = self.get(key); | ||
@@ -506,0 +534,0 @@ if( val instanceof Configuration ) { |
module.exports.Configuration = require('./config').Configuration; | ||
module.exports.ConfigContainer = require('./config-container').ConfigContainer; | ||
module.exports.ConfigContainer = require('./container').ConfigContainer; | ||
module.exports.ConfigFile = require('./file').ConfigFile; | ||
@@ -4,0 +4,0 @@ module.exports.ConfigArgs = require('./args').ConfigArgs; |
@@ -20,3 +20,3 @@ var parsers = require('./format').parsers, | ||
var org = this._org_ = this._options_.original; | ||
var org = this._org_ = this._options_.config; | ||
this._cache_ = {forward: {}, reverse: {}}; | ||
@@ -104,2 +104,7 @@ | ||
p.update = function(name,value,source) { | ||
// remapped objects are read only | ||
// this._org_.set( this.map_new_name(name), value, source); | ||
}; | ||
p.set = function(name,value,source) { | ||
@@ -106,0 +111,0 @@ // remapped objects are read only |
{ | ||
"name": "gestalt", | ||
"version": "0.0.7", | ||
"version": "0.0.8", | ||
"author": "Chris Howe <chris@howeville.com>", | ||
@@ -5,0 +5,0 @@ "description": "Event driven configuration management.", |
269
README.md
@@ -6,23 +6,24 @@ # gestalt | ||
underlying configuration for an application may change while the | ||
application is still running. Gestalt gives you a framework detecting | ||
application is still running. Gestalt provides a framework detecting | ||
and reacting to these changes without having to completely restart | ||
your application. | ||
There are a couple of motivations for gestalt. Configuration of a | ||
There are a couple of motivations for gestalt: Configuration of a | ||
large software system is often complicated - there are of course many | ||
tools out there for gathering configuration information from a bunch | ||
of different sources. nconf for node is a good one, and gestalt is to | ||
some extent based upon it, ( but also influenced by configliere for | ||
ruby and the configuration node structure of chef). There are a couple | ||
of things that many of these tools do not do. First, configuration | ||
files (and other sources) can change, and it would be nice to be able | ||
to react to these changes on-the-fly. Second, when you have a | ||
some extent based upon it. Gestalt is also influenced by configliere | ||
for ruby and the configuration node structure of chef. However, there | ||
are a couple of things that many of these tools do not do well. First, | ||
configuration files (and other sources) can change, and it would be | ||
nice to be able to react to these changes on-the-fly. Second, for a | ||
sufficiently complicated system of default and override configuration | ||
sources, it can become difficult to figure out exactly where a | ||
particular setting came from. Gestalt solves both of these | ||
problems. It has a per-value event change tracking system so that you | ||
can track changes to individual settings to your configuration. It | ||
also rigorously keeps track of where the values for particular | ||
settings came from. | ||
particular setting came from. | ||
Gestalt solves both of these problems. It has a per-value event change | ||
tracking system so that you can track changes to individual settings | ||
to your configuration. It also rigorously keeps track of where the | ||
values for particular settings came from. | ||
## Basics | ||
@@ -62,3 +63,3 @@ | ||
Values can be primative values (numbers, strings, booleans, | ||
etc.). Assignments of structured objects get destructured into nested | ||
etc.). By default, assignments of structured objects get destructured into nested | ||
Configuration objects. | ||
@@ -73,8 +74,9 @@ | ||
In many cases (not quite all...this is not yet supported for | ||
RemapConfig objects...) it is possible to turn a configuration object | ||
back into a regular object. In fact, if a configuration object looks | ||
like an array (all integer keys...) toObject will in fact return an | ||
array. | ||
( The default destructuring of assignments can be disabled. See | ||
the destructure_assignments and destructure_arrays options, below. ) | ||
It is possible to turn a configuration object back into a regular | ||
object. Also, if a configuration object looks like an array (all | ||
integer keys...) toObject will in fact return an array. | ||
## Events | ||
@@ -98,5 +100,5 @@ | ||
You can also listen to events on the nested configuration objects. Note | ||
that configuration names in the events are reported relative to the configuration | ||
object you are listening to. | ||
The nested configuration objects are also event emitters. Note that | ||
the configuration names in the events are reported relative to the | ||
configuration object being listening to. | ||
@@ -134,3 +136,4 @@ Configuration objects also have a ready/invalid/other state. When | ||
configuration data. You can read data from it, and set up | ||
listeners. | ||
listeners. Note that a configuration can only be in a ready | ||
state if all of its dependencies are also ready. | ||
@@ -143,3 +146,4 @@ - 'invalid' Something has gone wrong. We may not be able to get in | ||
stop consuming configuration data from the object and seek a way to remedy | ||
the invalid state. | ||
the invalid state. If any of an objects depencencies is in an invalid | ||
state, the object will also be invalid. | ||
@@ -256,3 +260,3 @@ - 'not ready', (and others). The configuration object is not ready for reads. | ||
### Configuration | ||
### Configuration | ||
@@ -305,5 +309,7 @@ A Configuration object is a container of name value pairs. The names | ||
#### Methods | ||
- get(name) | ||
Returns the value assigned to `name`, or undefined if not | ||
Returns the value assigned to 'name', or undefined if not | ||
present. Namespaces are separated by colons. If name is an array, it | ||
@@ -352,5 +358,5 @@ is treated the same as if it were a single string joined together by | ||
- report() | ||
- report( ) | ||
Generates a detailed report of all of the names. | ||
Generates a detailed report (on console.log) of all of the names. | ||
@@ -363,3 +369,4 @@ - toObject() | ||
are sequential numbers starting with 0, it will be converted into an | ||
array instead of a regular object. | ||
array instead of a regular object. The resultant object will not | ||
generate any events and will not have source attribution. | ||
@@ -376,7 +383,8 @@ - options( options ) | ||
Calls the callback whenever there is a change to the configuration that | ||
matches the pattern. Pattern could be a string, or a regex, in which cases | ||
match means that the name of the changed value either equals the string | ||
or matches the regex respectively. Pattern could also be a function which | ||
return a truthy value if the change matches its criteria. | ||
Calls the callback whenever there is a change to the configuration | ||
that matches the pattern. Pattern could be a string, or a regex, in | ||
which cases match means that the name of the changed value either | ||
equals the string or matches the regex respectively. Pattern could | ||
also be a function which return a truthy value if the change matches | ||
its criteria. | ||
@@ -400,5 +408,5 @@ - removePatternListener( callback ) | ||
Change events are emitted whenever the Configuration object detects that something | ||
has changed in the data. The handlers to these events are passed an object describing | ||
the change: | ||
Change events are emitted whenever the Configuration object detects | ||
that something has changed in the data. The handlers to these events | ||
are passed an object describing the change: | ||
@@ -414,5 +422,190 @@ ```javascript | ||
When the object's state changes between 'invalid', 'ready' and 'not ready', it will | ||
emit 'change' events. | ||
When the object's state changes between 'invalid', 'ready' and 'not | ||
ready', it will emit 'state' events of the form: | ||
```javascript | ||
state = { state: 'ready', // new state value | ||
old_state: 'invalid', // state we are transitioning to | ||
data: 'good now' // data passed in by the config.state() method. | ||
}; | ||
``` | ||
### ConfigContainer | ||
gestalt's mechanism to deal with the notion of default and override | ||
behavior is implemented in the ConfigContainer object. ConfigContainers | ||
contain a list or priority ordered configuration objects. Calling 'get(name)' | ||
on the ConfigContainer will return the value assigned to the highest priority | ||
configuration that has a value set for that name. | ||
- constructor ConfigContainer( options ) | ||
Takes the same options as a Configuration object, but will also accept | ||
-config | ||
A configuration object to act as the 'normal' object. See 'set' and | ||
'update' below. If no config option is given, a new object will be | ||
created as the 'normal' object; | ||
#### Methods | ||
All of the public methods of the Configuration object should work on a | ||
ConfigContainer, however the semantics of a few of them are a little | ||
different. | ||
- get(name) | ||
Returns the value associated with the name of the highest priority object | ||
containing a defined value for the name. | ||
- set(name, value, source) | ||
Calls 'set' on the 'normal' priority configuration object. | ||
- update(name, value, source) | ||
Calls 'update' on the highest prior configuration that has a value | ||
already set for 'name'. If none have defined 'name', then call 'update' | ||
on the 'normal' priority object. | ||
- keys() | ||
Returns the union of all of the keys in all of the contained configuration | ||
objects. | ||
- has(name) | ||
Returns true if the name is in the result of calling keys() | ||
- each() | ||
Works the same as Configuration's each method, but the values are as | ||
determined by the highest priority configuration object that has a | ||
value for the given key. | ||
- addOverride( config ) | ||
Adds a configuration object to the priority list of configurations | ||
as the highest priority object. | ||
- addDefault( config ) | ||
Adds a configuration object to the priority list as the lowest | ||
priority config object. | ||
### RemapConfig | ||
The RemapConfig object provides a way to change the names of a | ||
configuration without changing the values. This useful for changing | ||
the names that come from environment variables or command line | ||
variables into names that match up with the configuration hierarchy | ||
established in a configuraiton file - making it possible to use | ||
the override and default behaviors from a ConfigContainer object. | ||
- constructor RemapConfig( options ) | ||
Takes the same options as a Configuration object, but will also accept | ||
-config (mandatory) | ||
The configuration object to remap. | ||
-mapper (mandatory) | ||
Specifies how to remap configuration names. This can either be a | ||
function of the form: | ||
```javascript | ||
remap = function(old_value) { | ||
var new_value = "a:b:c:" + old_value; | ||
return new_value; | ||
} | ||
``` | ||
or it can be a flat javascript object with old names as keys and new | ||
names as values. | ||
Names that get mapped to 'undefined' by a remap function, or that are | ||
not present in the remap object, will simply not be included in the | ||
resultant object. | ||
### Methods | ||
All of the Configuration public methods are supported, with the following | ||
additions and modifications: | ||
- original() | ||
Returns a reference to the unmapped configuration object. | ||
- set() | ||
Does nothing - remap objects are read only at this time. | ||
- update() | ||
Does nothing - remap objects are read only at this time. | ||
- remove() | ||
Does nothing - remap objects are read only at this time. | ||
### ConfigArgs | ||
This is a standard Configuration object that pulls its name and value | ||
pairs from parsing the command line arguments with the optimist | ||
library. In addition to the standard configuration options, it will also | ||
accept the following: | ||
- argv | ||
Use the array instead of the arguments in process.argv | ||
- optimist_usage | ||
String to pass on to optimist as a usage string. ( Uses optimist's | ||
"usage" method. ) | ||
- optimist_options | ||
Object to pass on to optimist as a configuration options. ( Uses | ||
optimist's "usage" method. ) | ||
### ConfigEnv | ||
This is a standard Configuration object that pulls its name and value | ||
pairs from parsing the environmental variables. In addition to the | ||
standard configuration options, it will also accept the following: | ||
- env | ||
Use this set of name value pairs instead of process.env. | ||
### ConfigFile | ||
This is a standard Configuration object that draws its names and values | ||
from a configuration file. In addition to the standard options, ConfigFiles | ||
accept the following: | ||
- format | ||
Tells what format the file is in. Current options are 'json', 'yaml' | ||
and 'ini'. | ||
- source | ||
Tells what file to read - gets passed to fs.readFile. | ||
- watch | ||
Boolean. If set to true, the constructed ConfigFile object will set up | ||
a watch for changes to the underlying file. If it changes on disk, | ||
ConfigFile will reload the file and update any changed values. | ||
@@ -342,2 +342,24 @@ var vows = require('vows'), | ||
} | ||
}, | ||
"State Change": { | ||
topic: function() { | ||
var states = []; | ||
var config = new Configuration({source: "X"}) | ||
.on('state', function(state) { states.push(state); } ); | ||
config.set("a:b:c:d",1); | ||
config.set("a:b:d:e",1); | ||
config.get("a:b:c").state('invalid', 'test1'); | ||
config.get("a:b:d").state('invalid', 'test2'); | ||
config.get("a:b").state('unknown', 'test3'); | ||
config.get("a:b:c").state('ready', 'doesnt propagate'); | ||
config.get("a:b:d").state('ready', 'test4'); | ||
return states; | ||
}, | ||
"4 state changes": function(states) { | ||
assert.equal( states.length, 4 ); | ||
assert.deepEqual( _.pluck(states,'state'), | ||
['invalid','invalid','invalid', 'ready'] ); | ||
assert.deepEqual( _.pluck(states,'data'), | ||
['test1','test2','test3','test4' ] ); | ||
} | ||
} | ||
@@ -344,0 +366,0 @@ }).export(module); |
@@ -37,12 +37,2 @@ var vows = require('vows'), | ||
}, | ||
'should break apart colon separated paths': | ||
function(config) { | ||
var path = config.getPath("a:b:c:d"); | ||
assert.isArray( path ); | ||
assert.equal(path.length, 4); | ||
assert.equal(path[0], "a"); | ||
assert.equal(path[1], "b"); | ||
assert.equal(path[2], "c"); | ||
assert.equal(path[3], "d"); | ||
}, | ||
'should populate new paths when set': | ||
@@ -258,5 +248,62 @@ function(config) { | ||
} | ||
}, | ||
"State Change": { | ||
topic: function() { | ||
var states = []; | ||
var orig = new ConfigContainer(); | ||
var config = new ConfigContainer({config: orig}) | ||
.on('state', function(state) { states.push(state); } ); | ||
var def = new Configuration(); | ||
var over = new Configuration(); | ||
config.addDefault(def); | ||
config.addOverride(over); | ||
def.state('invalid', 'test1'); | ||
over.state('invalid', 'test2'); | ||
orig.state('unknown', 'test3'); | ||
orig.state('ready', 'doesnt propagate'); | ||
def.state('ready', 'doesnt propagate'); | ||
over.state('ready', 'test4'); | ||
return states; | ||
}, | ||
"4 state changes": function(states) { | ||
assert.equal( states.length, 4 ); | ||
assert.deepEqual( _.pluck(states,'state'), | ||
['invalid','invalid','invalid', 'ready'] ); | ||
assert.deepEqual( _.pluck(states,'data'), | ||
['test1','test2','test3','test4' ] ); | ||
} | ||
}, | ||
"Initial State": { | ||
topic: function() { | ||
var states = []; | ||
var orig = new ConfigContainer(); | ||
orig.state('not ready'); | ||
var config = new ConfigContainer({config: orig}); | ||
states.push( config.state() ); | ||
var def = new Configuration(); | ||
def.state('invalid') | ||
var over = new Configuration(); | ||
over.state('unknown'); | ||
config.addDefault(def); | ||
states.push( config.state() ); | ||
config.addOverride(over); | ||
states.push( config.state() ); | ||
return states; | ||
}, | ||
"states": function(states) { | ||
assert.deepEqual( states, | ||
['ready','invalid','invalid'] ); | ||
} | ||
} | ||
}).export(module); | ||
@@ -23,3 +23,3 @@ var vows = require('vows'), | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
return remap; | ||
@@ -44,3 +44,3 @@ }, | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
config.set("a", 1); | ||
@@ -73,3 +73,3 @@ config.set("b", 2); | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
config.set("a", 1); | ||
@@ -97,3 +97,3 @@ config.set("b", 2); | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
var changes = []; | ||
@@ -149,3 +149,3 @@ remap.on('change', function( change ) { | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
var changes = []; | ||
@@ -185,3 +185,3 @@ remap.on('change', function( change ) { | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, original: config}); | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
var changes = []; | ||
@@ -204,3 +204,28 @@ remap.on('change', function( change ) { | ||
} | ||
}, | ||
"State Change": { | ||
topic: function() { | ||
var states = []; | ||
var config = new Configuration({source: "basic config object"}); | ||
var mapper = function(old_name) { | ||
return {a:'e:f', b:'e:g', c:'d'}[old_name]; | ||
}; | ||
var remap = new RemapConfig({mapper: mapper, config: config}); | ||
remap.on('state', function(state) {states.push(state);} ); | ||
config.state('invalid', 'test1'); | ||
config.state('invalid', 'test2'); | ||
config.state('weird', 'test3'); | ||
config.state('weird', 'doesnt propagate'); | ||
config.state('ready', 'test4'); | ||
return states; | ||
}, | ||
"4 state changes": function(states) { | ||
assert.equal( states.length, 4 ); | ||
assert.deepEqual( _.pluck(states,'state'), | ||
['invalid','invalid','weird','ready'] ); | ||
assert.deepEqual( _.pluck(states,'data'), | ||
['test1','test2','test3','test4' ] ); | ||
} | ||
} | ||
}).export(module); | ||
@@ -207,0 +232,0 @@ |
Sorry, the diff of this file is not supported yet
132095
37
2578
599